這篇文章主要參考了 Vue.js 核心成員 Guillaume Chau 在 19 年美國的 Vue conf 分享的主題:9 Performance secrets revealed,分享中提到了九個 Vue.js 性能優化的技巧。javascript
我看完他的分享 PPT後,也閱讀了相關的項目源碼,在深刻了解它的優化原理後,把其中一些優化技巧也應用到了我平時的工做中,取得了至關不錯的效果。html
這個分享可謂是很是實用了,可是知道和關注的人彷佛並很少,到目前爲止,該項目也只有可憐的幾百個 star。雖然距大佬的分享已經有兩年時間,可是其中的優化技巧並無過期,爲了讓更多的人瞭解並學習到其中的實用技巧,我決定對他的分享作二次加工,詳細闡述其中的優化原理,並作必定程度的擴展和延伸。前端
本文主要仍是針對 Vue.js 2.x 版本,畢竟接下來一段時間,Vue.js 2.x 仍是咱們工做中的主流版本。vue
我建議你在學習這篇文章的時候能夠拉取項目的源碼,而且本地運行,查看優化先後的效果差別。java
第一個技巧,函數式組件,你能夠查看這個在線示例。node
優化前的組件代碼以下:react
<template>
<div class="cell">
<div v-if="value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>
<script> export default { props: ['value'], } </script>
複製代碼
優化後的組件代碼以下:git
<template functional>
<div class="cell">
<div v-if="props.value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>
複製代碼
而後咱們在父組件各渲染優化先後的組件 800 個,並在每一幀內部經過修改數據來觸發組件的更新,開啓 Chrome 的 Performance 面板記錄它們的性能,獲得以下結果。github
優化前:算法
優化後:
對比這兩張圖咱們能夠看到優化前執行 script
的時間要多於優化後的,而咱們知道 JS 引擎是單線程的運行機制,JS 線程會阻塞 UI 線程,因此當腳本執行時間過長,就會阻塞渲染,致使頁面卡頓。而優化後的 script
執行時間短,因此它的性能更好。
那麼,爲何用函數式組件 JS 的執行時間就變短了呢?這要從函數式組件的實現原理提及了,你能夠把它理解成一個函數,它能夠根據你傳遞的上下文數據渲染生成一片 DOM。
函數式組件和普通的對象類型的組件不一樣,它不會被看做成一個真正的組件,咱們知道在 patch
過程當中,若是遇到一個節點是組件 vnode
,會遞歸執行子組件的初始化過程;而函數式組件的 render
生成的是普通的 vnode
,不會有遞歸子組件的過程,所以渲染開銷會低不少。
所以,函數式組件也不會有狀態,不會有響應式數據,生命週期鉤子函數這些東西。你能夠把它當成把普通組件模板中的一部分 DOM 剝離出來,經過函數的方式渲染出來,是一種在 DOM 層面的複用。
第二個技巧,子組件拆分,你能夠查看這個在線示例。
優化前的組件代碼以下:
<template>
<div :style="{ opacity: number / 300 }">
<div>{{ heavy() }}</div>
</div>
</template>
<script> export default { props: ['number'], methods: { heavy () { const n = 100000 let result = 0 for (let i = 0; i < n; i++) { result += Math.sqrt(Math.cos(Math.sin(42))) } return result } } } </script>
複製代碼
優化後的組件代碼以下:
<template>
<div :style="{ opacity: number / 300 }">
<ChildComp/>
</div>
</template>
<script> export default { components: { ChildComp: { methods: { heavy () { const n = 100000 let result = 0 for (let i = 0; i < n; i++) { result += Math.sqrt(Math.cos(Math.sin(42))) } return result }, }, render (h) { return h('div', this.heavy()) } } }, props: ['number'] } </script>
複製代碼
而後咱們在父組件各渲染優化先後的組件 300 個,並在每一幀內部經過修改數據來觸發組件的更新,開啓 Chrome 的 Performance 面板記錄它們的性能,獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們能夠看到優化後執行 script
的時間要明顯少於優化前的,所以性能體驗更好。
那麼爲何會有差別呢,咱們來看優化前的組件,示例經過一個 heavy
函數模擬了一個耗時的任務,且這個函數在每次渲染的時候都會執行一次,因此每次組件的渲染都會消耗較長的時間執行 JavaScript。
而優化後的方式是把這個耗時任務 heavy
函數的執行邏輯用子組件 ChildComp
封裝了,因爲 Vue 的更新是組件粒度的,雖然每一幀都經過數據修改致使了父組件的從新渲染,可是 ChildComp
卻不會從新渲染,由於它的內部也沒有任何響應式數據的變化。因此優化後的組件不會在每次渲染都執行耗時任務,天然執行的 JavaScript 時間就變少了。
不過針對這個優化的方式我提出了一些不一樣的見解,詳情能夠點開這個 issue,我認爲這個場景下的優化用計算屬性要比子組件拆分要好。得益於計算屬性自身緩存特性,耗時的邏輯也只會在第一次渲染的時候執行,並且使用計算屬性也沒有額外渲染子組件的開銷。
在實際工做中,使用計算屬性是優化性能的場景會有不少,畢竟它也體現了一種空間換時間的優化思想。
第三個技巧,局部變量,你能夠查看這個在線示例。
優化前的組件代碼以下:
<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>
<script> export default { props: ['start'], computed: { base () { return 42 }, result () { let result = this.start for (let i = 0; i < 1000; i++) { result += Math.sqrt(Math.cos(Math.sin(this.base))) + this.base * this.base + this.base + this.base * 2 + this.base * 3 } return result }, }, } </script>
複製代碼
優化後的組件代碼以下:
<template>
<div :style="{ opacity: start / 300 }">{{ result }}</div>
</template>
<script> export default { props: ['start'], computed: { base () { return 42 }, result ({ base, start }) { let result = start for (let i = 0; i < 1000; i++) { result += Math.sqrt(Math.cos(Math.sin(base))) + base * base + base + base * 2 + base * 3 } return result }, }, } </script>
複製代碼
而後咱們在父組件各渲染優化先後的組件 300 個,並在每一幀內部經過修改數據來觸發組件的更新,開啓 Chrome 的 Performance 面板記錄它們的性能,獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們能夠看到優化後執行 script
的時間要明顯少於優化前的,所以性能體驗更好。
這裏主要是優化先後的組件的計算屬性 result
的實現差別,優化前的組件屢次在計算過程當中訪問 this.base
,而優化後的組件會在計算前先用局部變量 base
,緩存 this.base
,後面直接訪問 base
。
那麼爲啥這個差別會形成性能上的差別呢,緣由是你每次訪問 this.base
的時候,因爲 this.base
是一個響應式對象,因此會觸發它的 getter
,進而會執行依賴收集相關邏輯代碼。相似的邏輯執行多了,像示例這樣,幾百次循環更新幾百個組件,每一個組件觸發 computed
從新計算,而後又屢次執行依賴收集相關邏輯,性能天然就降低了。
從需求上來講,this.base
執行一次依賴收集就夠了,把它的 getter
求值結果返回給局部變量 base
,後續再次訪問 base
的時候就不會觸發 getter
,也不會走依賴收集的邏輯了,性能天然就獲得了提高。
這是一個很是實用的性能優化技巧。由於不少人在開發 Vue.js 項目的時候,每當取變量的時候就習慣性直接寫 this.xxx
了,由於大部分人並不會注意到訪問 this.xxx
背後作的事情。在訪問次數很少的時候,性能問題並無凸顯,可是一旦訪問次數變多,好比在一個大循環中屢次訪問,相似示例這種場景,就會產生性能問題了。
我以前給 ZoomUI 的 Table 組件作性能優化的時候,在 render table body
的時候就使用了局部變量的優化技巧,並寫了 benchmark 作性能對比:渲染 1000 * 10 的表格,ZoomUI Table 的更新數據從新渲染的性能要比 ElementUI 的 Table 性能提高了近一倍。
第四個技巧,使用 v-show
複用 DOM,你能夠查看這個在線示例。
優化前的組件代碼以下:
<template functional>
<div class="cell">
<div v-if="props.value" class="on">
<Heavy :n="10000"/>
</div>
<section v-else class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>
複製代碼
優化後的組件代碼以下:
<template functional>
<div class="cell">
<div v-show="props.value" class="on">
<Heavy :n="10000"/>
</div>
<section v-show="!props.value" class="off">
<Heavy :n="10000"/>
</section>
</div>
</template>
複製代碼
而後咱們在父組件各渲染優化先後的組件 200 個,並在每一幀內部經過修改數據來觸發組件的更新,開啓 Chrome 的 Performance 面板記錄它們的性能,獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們能夠看到優化後執行 script
的時間要明顯少於優化前的,所以性能體驗更好。
優化先後的主要區別是用 v-show
指令替代了 v-if
指令來替代組件的顯隱,雖然從表現上看,v-show
和 v-if
相似,都是控制組件的顯隱,但內部實現差距仍是很大的。
v-if
指令在編譯階段就會編譯成一個三元運算符,條件渲染,好比優化前的組件模板通過編譯後生成以下渲染函數:
function render() {
with(this) {
return _c('div', {
staticClass: "cell"
}, [(props.value) ? _c('div', {
staticClass: "on"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1) : _c('section', {
staticClass: "off"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1)])
}
}
複製代碼
當條件 props.value
的值變化的時候,會觸發對應的組件更新,對於 v-if
渲染的節點,因爲新舊節點 vnode
不一致,在覈心 diff 算法比對過程當中,會移除舊的 vnode
節點,建立新的 vnode
節點,那麼就會建立新的 Heavy
組件,又會經歷 Heavy
組件自身初始化、渲染 vnode
、patch
等過程。
所以使用 v-if
每次更新組件都會建立新的 Heavy
子組件,當更新的組件多了,天然就會形成性能壓力。
而當咱們使用 v-show
指令,優化後的組件模板通過編譯後生成以下渲染函數:
function render() {
with(this) {
return _c('div', {
staticClass: "cell"
}, [_c('div', {
directives: [{
name: "show",
rawName: "v-show",
value: (props.value),
expression: "props.value"
}],
staticClass: "on"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1), _c('section', {
directives: [{
name: "show",
rawName: "v-show",
value: (!props.value),
expression: "!props.value"
}],
staticClass: "off"
}, [_c('Heavy', {
attrs: {
"n": 10000
}
})], 1)])
}
}
複製代碼
當條件 props.value
的值變化的時候,會觸發對應的組件更新,對於 v-show
渲染的節點,因爲新舊 vnode
一致,它們只須要一直 patchVnode
便可,那麼它又是怎麼讓 DOM 節點顯示和隱藏的呢?
原來在 patchVnode
過程當中,內部會對執行 v-show
指令對應的鉤子函數 update
,而後它會根據 v-show
指令綁定的值來設置它做用的 DOM 元素的 style.display
的值控制顯隱。
所以相比於 v-if
不斷刪除和建立函數新的 DOM,v-show
僅僅是在更新現有 DOM 的顯隱值,因此 v-show
的開銷要比 v-if
小的多,當其內部 DOM 結構越複雜,性能的差別就會越大。
可是 v-show
相比於 v-if
的性能優點是在組件的更新階段,若是僅僅是在初始化階段,v-if
性能還要高於 v-show
,緣由是在於它僅僅會渲染一個分支,而 v-show
把兩個分支都渲染了,經過 style.display
來控制對應 DOM 的顯隱。
在使用 v-show
的時候,全部分支內部的組件都會渲染,對應的生命週期鉤子函數都會執行,而使用 v-if
的時候,沒有命中的分支內部的組件是不會渲染的,對應的生命週期鉤子函數都不會執行。
所以你要搞清楚它們的原理以及差別,才能在不一樣的場景使用適合的指令。
第五個技巧,使用 KeepAlive
組件緩存 DOM,你能夠查看這個在線示例。
優化前的組件代碼以下:
<template>
<div id="app">
<router-view/>
</div>
</template>
複製代碼
優化後的組件代碼以下:
<template>
<div id="app">
<keep-alive>
<router-view/>
</keep-alive>
</div>
</template>
複製代碼
咱們點擊按鈕在 Simple page 和 Heavy Page 之間切換,會渲染不一樣的視圖,其中 Heavy Page 的渲染很是耗時。咱們開啓 Chrome 的 Performance 面板記錄它們的性能,而後分別在優化先後執行如上的操做,會獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們能夠看到優化後執行 script
的時間要明顯少於優化前的,所以性能體驗更好。
在非優化場景下,咱們每次點擊按鈕切換路由視圖,都會從新渲染一次組件,渲染組件就會通過組件初始化,render
、patch
等過程,若是組件比較複雜,或者嵌套較深,那麼整個渲染耗時就會很長。
而在使用 KeepAlive
後,被 KeepAlive
包裹的組件在通過第一次渲染後,的 vnode
以及 DOM 都會被緩存起來,而後再下一次再次渲染該組件的時候,直接從緩存中拿到對應的 vnode
和 DOM,而後渲染,並不須要再走一次組件初始化,render
和 patch
等一系列流程,減小了 script
的執行時間,性能更好。
可是使用 KeepAlive
組件並不是沒有成本,由於它會佔用更多的內存去作緩存,這是一種典型的空間換時間優化思想的應用。
第六個技巧,使用 Deferred
組件延時分批渲染組件,你能夠查看這個在線示例。
優化前的組件代碼以下:
<template>
<div class="deferred-off">
<VueIcon icon="fitness_center" class="gigantic"/>
<h2>I'm an heavy page</h2>
<Heavy v-for="n in 8" :key="n"/>
<Heavy class="super-heavy" :n="9999999"/>
</div>
</template>
複製代碼
優化後的組件代碼以下:
<template>
<div class="deferred-on">
<VueIcon icon="fitness_center" class="gigantic"/>
<h2>I'm an heavy page</h2>
<template v-if="defer(2)">
<Heavy v-for="n in 8" :key="n"/>
</template>
<Heavy v-if="defer(3)" class="super-heavy" :n="9999999"/>
</div>
</template>
<script> import Defer from '@/mixins/Defer' export default { mixins: [ Defer(), ], } </script>
複製代碼
咱們點擊按鈕在 Simple page 和 Heavy Page 之間切換,會渲染不一樣的視圖,其中 Heavy Page 的渲染很是耗時。咱們開啓 Chrome 的 Performance 面板記錄它們的性能,而後分別在優化先後執行如上的操做,會獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們能夠發現,優化前當咱們從 Simple Page 切到 Heavy Page 的時候,在一次 Render 接近結尾的時候,頁面渲染的仍然是 Simple Page,會給人一種頁面卡頓的感受。而優化後當咱們從 Simple Page 切到 Heavy Page 的時候,在一次 Render 靠前的位置頁面就已經渲染了 Heavy Page 了,而且 Heavy Page 是漸進式渲染出來的。
優化先後的差距主要是後者使用了 Defer
這個 mixin
,那麼它具體是怎麼工做的,咱們來一探究竟:
export default function (count = 10) {
return {
data () {
return {
displayPriority: 0
}
},
mounted () {
this.runDisplayPriority()
},
methods: {
runDisplayPriority () {
const step = () => {
requestAnimationFrame(() => {
this.displayPriority++
if (this.displayPriority < count) {
step()
}
})
}
step()
},
defer (priority) {
return this.displayPriority >= priority
}
}
}
}
複製代碼
Defer
的主要思想就是把一個組件的一次渲染拆成屢次,它內部維護了 displayPriority
變量,而後在經過 requestAnimationFrame
在每一幀渲染的時候自增,最多加到 count
。而後使用 Defer mixin
的組件內部就能夠經過 v-if="defer(xxx)"
的方式來控制在 displayPriority
增長到 xxx
的時候渲染某些區塊了。
當你有渲染耗時的組件,使用 Deferred
作漸進式渲染是不錯的注意,它能避免一次 render
因爲 JS 執行時間過長致使渲染卡住的現象。
第七個技巧,使用 Time slicing
時間片切割技術,你能夠查看這個在線示例。
優化前的代碼以下:
fetchItems ({ commit }, { items }) {
commit('clearItems')
commit('addItems', items)
}
複製代碼
優化後的代碼以下:
fetchItems ({ commit }, { items, splitCount }) {
commit('clearItems')
const queue = new JobQueue()
splitArray(items, splitCount).forEach(
chunk => queue.addJob(done => {
// 分時間片提交數據
requestAnimationFrame(() => {
commit('addItems', chunk)
done()
})
})
)
await queue.start()
}
複製代碼
咱們先經過點擊 Genterate items
按鈕建立 10000 條假數據,而後分別在開啓和關閉 Time-slicing
的狀況下點擊 Commit items
按鈕提交數據,開啓 Chrome 的 Performance 面板記錄它們的性能,會獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們能夠發現,優化前總的 script
執行時間要比優化後的還要少一些,可是從實際的觀感上看,優化前點擊提交按鈕,頁面會卡死 1.2 秒左右,在優化後,頁面不會徹底卡死,但仍然會有渲染卡頓的感受。
那麼爲何在優化前頁面會卡死呢?由於一次性提交的數據過多,內部 JS 執行時間過長,阻塞了 UI 線程,致使頁面卡死。
優化後,頁面仍有卡頓,是由於咱們拆分數據的粒度是 1000 條,這種狀況下,從新渲染組件仍然有壓力,咱們觀察 fps 只有十幾,會有卡頓感。一般只要讓頁面的 fps 達到 60,頁面就會很是流暢,若是咱們把數據拆分粒度變成 100 條,基本上 fps 能達到 50 以上,雖然頁面渲染變流暢了,可是完成 10000 條數據總的提交時間仍是變長了。
使用 Time slicing
技術能夠避免頁面卡死,一般咱們在這種耗時任務處理的時候會加一個 loading 效果,在這個示例中,咱們能夠開啓 loading animation
,而後提交數據。對比發現,優化前因爲一次性提交數據過多,JS 一直長時間運行,阻塞 UI 線程,這個 loading 動畫是不會展現的,而優化後,因爲咱們拆成多個時間片去提交數據,單次 JS 運行時間變短了,這樣 loading 動畫就有機會展現了。
這裏要注意的一點,雖然咱們拆時間片使用了
requestAnimationFrame
API,可是使用requestAnimationFrame
自己是不能保證滿幀運行的,requestAnimationFrame
保證的是在瀏覽器每一次重繪後會執行對應傳入的回調函數,想要保證滿幀,只能讓 JS 在一個 Tick 內的運行時間不超過 17ms。
第八個技巧,使用 Non-reactive data
非響應式數據,你能夠查看這個在線示例。
優化前代碼以下:
const data = items.map(
item => ({
id: uid++,
data: item,
vote: 0
})
)
複製代碼
優化後代碼以下:
const data = items.map(
item => optimizeItem(item)
)
function optimizeItem (item) {
const itemData = {
id: uid++,
vote: 0
}
Object.defineProperty(itemData, 'data', {
// Mark as non-reactive
configurable: false,
value: item
})
return itemData
}
複製代碼
仍是前面的示例,咱們先經過點擊 Genterate items
按鈕建立 10000 條假數據,而後分別在開啓和關閉 Partial reactivity
的狀況下點擊 Commit items
按鈕提交數據,開啓 Chrome 的 Performance 面板記錄它們的性能,會獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們能夠看到優化後執行 script
的時間要明顯少於優化前的,所以性能體驗更好。
之因此有這種差別,是由於內部提交的數據的時候,會默認把新提交的數據也定義成響應式,若是數據的子屬性是對象形式,還會遞歸讓子屬性也變成響應式,所以當提交數據不少的時候,這個過程就變成了一個耗時過程。
而優化後咱們把新提交的數據中的對象屬性 data
手動變成了 configurable
爲 false
,這樣內部在 walk
時經過 Object.keys(obj)
獲取對象屬性數組會忽略 data
,也就不會爲 data
這個屬性 defineReactive
,因爲 data
指向的是一個對象,這樣也就會減小遞歸響應式的邏輯,至關於減小了這部分的性能損耗。數據量越大,這種優化的效果就會更明顯。
其實相似這種優化的方式還有不少,好比咱們在組件中定義的一些數據,也不必定都要在 data
中定義。有些數據咱們並非用在模板中,也不須要監聽它的變化,只是想在組件的上下文中共享這個數據,這個時候咱們能夠僅僅把這個數據掛載到組件實例 this
上,例如:
export default {
created() {
this.scroll = null
},
mounted() {
this.scroll = new BScroll(this.$el)
}
}
複製代碼
這樣咱們就能夠在組件上下文中共享 scroll
對象了,儘管它不是一個響應式對象。
第九個技巧,使用 Virtual scrolling
虛擬滾動組件,你能夠查看這個在線示例。
優化前組件的代碼以下:
<div class="items no-v">
<FetchItemViewFunctional v-for="item of items" :key="item.id" :item="item" @vote="voteItem(item)" />
</div>
複製代碼
優化後代碼以下:
<recycle-scroller class="items" :items="items" :item-size="24" >
<template v-slot="{ item }">
<FetchItemView :item="item" @vote="voteItem(item)" />
</template>
</recycle-scroller>
複製代碼
仍是前面的示例,咱們須要開啓 View list
,而後點擊 Genterate items
按鈕建立 10000 條假數據(注意,線上示例最多隻能建立 1000 條數據,實際上 1000 條數據並不能很好地體現優化的效果,因此我修改了源碼的限制,本地運行,建立了 10000 條數據),而後分別在 Unoptimized
和 RecycleScroller
的狀況下點擊 Commit items
按鈕提交數據,滾動頁面,開啓 Chrome 的 Performance 面板記錄它們的性能,會獲得以下結果。
優化前:
優化後:
對比這兩張圖咱們發現,在非優化的狀況下,10000 條數據在滾動狀況下 fps 只有個位數,在非滾動狀況下也就十幾,緣由是非優化場景下渲染的 DOM 太多,渲染自己的壓力很大。優化後,即便 10000 條數據,在滾動狀況下的 fps 也能有 30 多,在非滾動狀況下能夠達到 60 滿幀。
之因此有這個差別,是由於虛擬滾動的實現方式,是隻渲染視口內的 DOM,這樣總共渲染的 DOM 數量就不多了,天然性能就會好不少。
虛擬滾動組件也是 Guillaume Chau 寫的,感興趣的同窗能夠去研究它的源碼實現。它的基本原理就是監聽滾動事件,動態更新須要顯示的 DOM 元素,計算出它們在視圖中的位移。
虛擬滾動組件也並不是沒有成本,由於它須要在滾動的過程當中實時去計算,因此會有必定的 script
執行的成本。所以若是列表的數據量不是很大的狀況,咱們使用普通的滾動就足夠了。
經過這篇文章,我但願你能瞭解到 Vue.js 的九種性能優化技巧,並能運用到實際的開發項目中。除了上述技巧以外,還有懶加載圖片、懶加載組件、異步組件等等經常使用的性能優化手段。
在作性能優化前,咱們須要分析性能的瓶頸在哪,才能因地制宜。另外,性能優化都須要數據支撐的,你在作任何性能優化前,須要先採集優化前的數據,這樣優化後纔可以經過數據對比看到優化的效果。
但願你在往後的開發過程當中,再也不只知足於實現需求,寫每一行代碼的時候,都能思考它可能產生的性能方面的影響。
本文首發於公衆號「老黃的前端私房菜」,歡迎關注。