Vue.js性能優(yōu)化N個(gè)技巧(值得收藏)
這篇文章主要參考了 Vue.js 核心成員Guillaume Chau 在 19 年美國的 Vue conf 分享的主題:9 Performance secrets revealed,分享中提到了九個(gè) Vue.js 性能優(yōu)化的技巧。
我看完他的分享 PPT后,也閱讀了相關(guān)的項(xiàng)目源碼,在深入了解它的優(yōu)化原理后,把其中一些優(yōu)化技巧也應(yīng)用到了我平時(shí)的工作中,取得了相當(dāng)不錯(cuò)的效果。
這個(gè)分享可謂是非常實(shí)用了,但是知道和關(guān)注的人似乎并不多,到目前為止,該項(xiàng)目也只有可憐的幾百個(gè) star。雖然距大佬的分享已經(jīng)有兩年時(shí)間,但是其中的優(yōu)化技巧并沒有過時(shí),為了讓更多的人了解并學(xué)習(xí)到其中的實(shí)用技巧,我決定對(duì)他的分享做二次加工,詳細(xì)闡述其中的優(yōu)化原理,并做一定程度的擴(kuò)展和延伸。
本文主要還是針對(duì) Vue.js 2.x 版本,畢竟接下來一段時(shí)間,Vue.js 2.x 還是我們工作中的主流版本。
我建議你在學(xué)習(xí)這篇文章的時(shí)候可以拉取項(xiàng)目的源碼,并且本地運(yùn)行,查看優(yōu)化前后的效果差異。
Functional components
第一個(gè)技巧,函數(shù)式組件,你可以查看這個(gè)在線示例
優(yōu)化前的組件代碼如下:
<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>
優(yōu)化后的組件代碼如下:
<template functional>
<div class="cell">
<div v-if="props.value" class="on"></div>
<section v-else class="off"></section>
</div>
</template>
然后我們?cè)诟附M件各渲染優(yōu)化前后的組件 800 個(gè),并在每一幀內(nèi)部通過修改數(shù)據(jù)來觸發(fā)組件的更新,開啟 Chrome 的 Performance 面板記錄它們的性能,得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們可以看到優(yōu)化前執(zhí)行 script 的時(shí)間要多于優(yōu)化后的,而我們知道 JS 引擎是單線程的運(yùn)行機(jī)制,JS 線程會(huì)阻塞 UI 線程,所以當(dāng)腳本執(zhí)行時(shí)間過長,就會(huì)阻塞渲染,導(dǎo)致頁面卡頓。而優(yōu)化后的 script 執(zhí)行時(shí)間短,所以它的性能更好。
那么,為什么用函數(shù)式組件 JS 的執(zhí)行時(shí)間就變短了呢?這要從函數(shù)式組件的實(shí)現(xiàn)原理說起了,你可以把它理解成一個(gè)函數(shù),它可以根據(jù)你傳遞的上下文數(shù)據(jù)渲染生成一片 DOM。
函數(shù)式組件和普通的對(duì)象類型的組件不同,它不會(huì)被看作成一個(gè)真正的組件,我們知道在 patch 過程中,如果遇到一個(gè)節(jié)點(diǎn)是組件 vnode,會(huì)遞歸執(zhí)行子組件的初始化過程;而函數(shù)式組件的 render 生成的是普通的 vnode,不會(huì)有遞歸子組件的過程,因此渲染開銷會(huì)低很多。
因此,函數(shù)式組件也不會(huì)有狀態(tài),不會(huì)有響應(yīng)式數(shù)據(jù),生命周期鉤子函數(shù)這些東西。你可以把它當(dāng)成把普通組件模板中的一部分 DOM 剝離出來,通過函數(shù)的方式渲染出來,是一種在 DOM 層面的復(fù)用。
Child component splitting
第二個(gè)技巧,子組件拆分,你可以查看這個(gè)在線示例。
優(yōu)化前的組件代碼如下:
<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>
優(yōu)化后的組件代碼如下:
<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>
然后我們?cè)诟附M件各渲染優(yōu)化前后的組件 300 個(gè),并在每一幀內(nèi)部通過修改數(shù)據(jù)來觸發(fā)組件的更新,開啟 Chrome 的 Performance 面板記錄它們的性能,得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們可以看到優(yōu)化后執(zhí)行 script 的時(shí)間要明顯少于優(yōu)化前的,因此性能體驗(yàn)更好。
那么為什么會(huì)有差異呢,我們來看優(yōu)化前的組件,示例通過一個(gè) heavy 函數(shù)模擬了一個(gè)耗時(shí)的任務(wù),且這個(gè)函數(shù)在每次渲染的時(shí)候都會(huì)執(zhí)行一次,所以每次組件的渲染都會(huì)消耗較長的時(shí)間執(zhí)行 JavaScript。
而優(yōu)化后的方式是把這個(gè)耗時(shí)任務(wù) heavy 函數(shù)的執(zhí)行邏輯用子組件 ChildComp 封裝了,由于 Vue 的更新是組件粒度的,雖然每一幀都通過數(shù)據(jù)修改導(dǎo)致了父組件的重新渲染,但是 ChildComp 卻不會(huì)重新渲染,因?yàn)樗膬?nèi)部也沒有任何響應(yīng)式數(shù)據(jù)的變化。所以優(yōu)化后的組件不會(huì)在每次渲染都執(zhí)行耗時(shí)任務(wù),自然執(zhí)行的 JavaScript 時(shí)間就變少了。
不過針對(duì)這個(gè)優(yōu)化的方式我提出了一些不同的看法,詳情可以點(diǎn)開這個(gè) issue,我認(rèn)為這個(gè)場景下的優(yōu)化用計(jì)算屬性要比子組件拆分要好。得益于計(jì)算屬性自身緩存特性,耗時(shí)的邏輯也只會(huì)在第一次渲染的時(shí)候執(zhí)行,而且使用計(jì)算屬性也沒有額外渲染子組件的開銷。
在實(shí)際工作中,使用計(jì)算屬性是優(yōu)化性能的場景會(huì)有很多,畢竟它也體現(xiàn)了一種空間換時(shí)間的優(yōu)化思想。
Local variables
第三個(gè)技巧,局部變量,你可以查看這個(gè)在線示例。
優(yōu)化前的組件代碼如下:
<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>
優(yōu)化后的組件代碼如下:
<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>
然后我們?cè)诟附M件各渲染優(yōu)化前后的組件 300 個(gè),并在每一幀內(nèi)部通過修改數(shù)據(jù)來觸發(fā)組件的更新,開啟 Chrome 的 Performance 面板記錄它們的性能,得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們可以看到優(yōu)化后執(zhí)行 script 的時(shí)間要明顯少于優(yōu)化前的,因此性能體驗(yàn)更好。
這里主要是優(yōu)化前后組件的計(jì)算屬性 result 的實(shí)現(xiàn)差異,優(yōu)化前的組件多次在計(jì)算過程中訪問 this.base,而優(yōu)化后的組件會(huì)在計(jì)算前先用局部變量 base 緩存 this.base,后面則直接訪問 base變量。
那么為啥這個(gè)差異會(huì)造成性能上的差異呢,原因是你每次訪問 this.base 的時(shí)候,由于 this.base 是一個(gè)響應(yīng)式對(duì)象,所以會(huì)觸發(fā)它的 getter,進(jìn)而會(huì)執(zhí)行依賴收集相關(guān)邏輯代碼。類似的邏輯執(zhí)行多了,像示例這樣,幾百次循環(huán)更新幾百個(gè)組件,每個(gè)組件觸發(fā) computed 重新計(jì)算,然后又多次執(zhí)行依賴收集相關(guān)邏輯,性能自然就下降了。
從需求上來說,this.base 執(zhí)行一次依賴收集就夠了,因此我們只需要把它的 getter 求值結(jié)果返回給局部變量 base,后續(xù)再次訪問 base 的時(shí)候就不會(huì)觸發(fā) getter,也不會(huì)走依賴收集的邏輯了,性能自然就得到了提升。
這是一個(gè)非常實(shí)用的性能優(yōu)化技巧。因?yàn)楹芏嗳嗽陂_發(fā) Vue.js 項(xiàng)目的時(shí)候,每當(dāng)取變量的時(shí)候就習(xí)慣性直接寫 this.xxx 了,因?yàn)榇蟛糠秩瞬⒉粫?huì)注意到訪問 this.xxx 背后做的事情。在訪問次數(shù)不多的時(shí)候,性能問題并沒有凸顯,但是一旦訪問次數(shù)變多,比如在一個(gè)大循環(huán)中多次訪問,類似示例這種場景,就會(huì)產(chǎn)生性能問題了。
我之前給 ZoomUI 的 Table 組件做性能優(yōu)化的時(shí)候,在 render table body 的時(shí)候就使用了局部變量的優(yōu)化技巧,并寫了 benchmark 做性能對(duì)比:渲染 1000 * 10 的表格,ZoomUI Table 的更新數(shù)據(jù)重新渲染的性能要比 ElementUI 的 Table 性能提升了近一倍。
Reuse DOM with v-show
第四個(gè)技巧,使用 v-show 復(fù)用 DOM,你可以查看這個(gè)在線示例。
優(yōu)化前的組件代碼如下:
<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>
優(yōu)化后的組件代碼如下:
<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>
然后我們?cè)诟附M件各渲染優(yōu)化前后的組件 200 個(gè),并在每一幀內(nèi)部通過修改數(shù)據(jù)來觸發(fā)組件的更新,開啟 Chrome 的 Performance 面板記錄它們的性能,得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們可以看到優(yōu)化后執(zhí)行 script 的時(shí)間要明顯少于優(yōu)化前的,因此性能體驗(yàn)更好。
優(yōu)化前后的主要區(qū)別是用 v-show 指令替代了 v-if 指令來替代組件的顯隱,雖然從表現(xiàn)上看,v-show 和 v-if 類似,都是控制組件的顯隱,但內(nèi)部實(shí)現(xiàn)差距還是很大的。
v-if 指令在編譯階段就會(huì)編譯成一個(gè)三元運(yùn)算符,條件渲染,比如優(yōu)化前的組件模板經(jīng)過編譯后生成如下渲染函數(shù):
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)])
}
}
當(dāng)條件 props.value 的值變化的時(shí)候,會(huì)觸發(fā)對(duì)應(yīng)的組件更新,對(duì)于 v-if 渲染的節(jié)點(diǎn),由于新舊節(jié)點(diǎn) vnode 不一致,在核心 diff 算法比對(duì)過程中,會(huì)移除舊的 vnode 節(jié)點(diǎn),創(chuàng)建新的 vnode 節(jié)點(diǎn),那么就會(huì)創(chuàng)建新的 Heavy 組件,又會(huì)經(jīng)歷 Heavy 組件自身初始化、渲染 vnode、patch 等過程。
因此使用 v-if 每次更新組件都會(huì)創(chuàng)建新的 Heavy 子組件,當(dāng)更新的組件多了,自然就會(huì)造成性能壓力。
而當(dāng)我們使用 v-show 指令,優(yōu)化后的組件模板經(jīng)過編譯后生成如下渲染函數(shù):
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)])
}
}
當(dāng)條件 props.value 的值變化的時(shí)候,會(huì)觸發(fā)對(duì)應(yīng)的組件更新,對(duì)于 v-show 渲染的節(jié)點(diǎn),由于新舊 vnode 一致,它們只需要一直 patchVnode 即可,那么它又是怎么讓 DOM 節(jié)點(diǎn)顯示和隱藏的呢?
原來在 patchVnode 過程中,內(nèi)部會(huì)對(duì)執(zhí)行 v-show 指令對(duì)應(yīng)的鉤子函數(shù) update,然后它會(huì)根據(jù) v-show 指令綁定的值來設(shè)置它作用的 DOM 元素的 style.display 的值控制顯隱。
因此相比于 v-if 不斷刪除和創(chuàng)建函數(shù)新的 DOM,v-show 僅僅是在更新現(xiàn)有 DOM 的顯隱值,所以 v-show 的開銷要比 v-if 小的多,當(dāng)其內(nèi)部 DOM 結(jié)構(gòu)越復(fù)雜,性能的差異就會(huì)越大。
但是 v-show 相比于 v-if 的性能優(yōu)勢是在組件的更新階段,如果僅僅是在初始化階段,v-if 性能還要高于 v-show,原因是在于它僅僅會(huì)渲染一個(gè)分支,而 v-show 把兩個(gè)分支都渲染了,通過 style.display 來控制對(duì)應(yīng) DOM 的顯隱。
在使用 v-show 的時(shí)候,所有分支內(nèi)部的組件都會(huì)渲染,對(duì)應(yīng)的生命周期鉤子函數(shù)都會(huì)執(zhí)行,而使用 v-if 的時(shí)候,沒有命中的分支內(nèi)部的組件是不會(huì)渲染的,對(duì)應(yīng)的生命周期鉤子函數(shù)都不會(huì)執(zhí)行。
因此你要搞清楚它們的原理以及差異,才能在不同的場景使用適合的指令。
KeepAlive
第五個(gè)技巧,使用 KeepAlive 組件緩存 DOM,你可以查看這個(gè)在線示例。
優(yōu)化前的組件代碼如下:
<template>
<div id="app">
<router-view/>
</div>
</template>
優(yōu)化后的組件代碼如下:
<template>
<div id="app">
<keep-alive>
<router-view/>
</keep-alive>
</div>
</template>
我們點(diǎn)擊按鈕在 Simple page 和 Heavy Page 之間切換,會(huì)渲染不同的視圖,其中 Heavy Page 的渲染非常耗時(shí)。我們開啟 Chrome 的 Performance 面板記錄它們的性能,然后分別在優(yōu)化前后執(zhí)行如上的操作,會(huì)得到如下結(jié)果。
優(yōu)化前:


優(yōu)化后:


對(duì)比這兩張圖我們可以看到優(yōu)化后執(zhí)行 script 的時(shí)間要明顯少于優(yōu)化前的,因此性能體驗(yàn)更好。
在非優(yōu)化場景下,我們每次點(diǎn)擊按鈕切換路由視圖,都會(huì)重新渲染一次組件,渲染組件就會(huì)經(jīng)過組件初始化,render、patch 等過程,如果組件比較復(fù)雜,或者嵌套較深,那么整個(gè)渲染耗時(shí)就會(huì)很長。
而在使用 KeepAlive 后,被 KeepAlive 包裹的組件在經(jīng)過第一次渲染后,的 vnode 以及 DOM 都會(huì)被緩存起來,然后再下一次再次渲染該組件的時(shí)候,直接從緩存中拿到對(duì)應(yīng)的 vnode 和 DOM,然后渲染,并不需要再走一次組件初始化,render 和 patch 等一系列流程,減少了 script 的執(zhí)行時(shí)間,性能更好。
但是使用 KeepAlive 組件并非沒有成本,因?yàn)樗鼤?huì)占用更多的內(nèi)存去做緩存,這是一種典型的空間換時(shí)間優(yōu)化思想的應(yīng)用。
Deferred features
第六個(gè)技巧,使用 Deferred 組件延時(shí)分批渲染組件,你可以查看這個(gè)在線示例。
優(yōu)化前的組件代碼如下:
<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>
優(yōu)化后的組件代碼如下:
<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>
我們點(diǎn)擊按鈕在 Simple page 和 Heavy Page 之間切換,會(huì)渲染不同的視圖,其中 Heavy Page 的渲染非常耗時(shí)。我們開啟 Chrome 的 Performance 面板記錄它們的性能,然后分別在優(yōu)化前后執(zhí)行如上的操作,會(huì)得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們可以發(fā)現(xiàn),優(yōu)化前當(dāng)我們從 Simple Page 切到 Heavy Page 的時(shí)候,在一次 Render 接近結(jié)尾的時(shí)候,頁面渲染的仍然是 Simple Page,會(huì)給人一種頁面卡頓的感覺。而優(yōu)化后當(dāng)我們從 Simple Page 切到 Heavy Page 的時(shí)候,在一次 Render 靠前的位置頁面就已經(jīng)渲染了 Heavy Page 了,并且 Heavy Page 是漸進(jìn)式渲染出來的。
優(yōu)化前后的差距主要是后者使用了 Defer 這個(gè) 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 的主要思想就是把一個(gè)組件的一次渲染拆成多次,它內(nèi)部維護(hù)了 displayPriority 變量,然后在通過 requestAnimationFrame 在每一幀渲染的時(shí)候自增,最多加到 count。然后使用 Defer mixin 的組件內(nèi)部就可以通過 v-if="defer(xxx)" 的方式來控制在 displayPriority 增加到 xxx 的時(shí)候渲染某些區(qū)塊了。
當(dāng)你有渲染耗時(shí)的組件,使用 Deferred 做漸進(jìn)式渲染是不錯(cuò)的注意,它能避免一次 render 由于 JS 執(zhí)行時(shí)間過長導(dǎo)致渲染卡住的現(xiàn)象。
Time slicing
第七個(gè)技巧,使用 Time slicing 時(shí)間片切割技術(shù),你可以查看這個(gè)在線示例。
優(yōu)化前的代碼如下:
fetchItems ({ commit }, { items }) {
commit('clearItems')
commit('addItems', items)
}
優(yōu)化后的代碼如下:
fetchItems ({ commit }, { items, splitCount }) {
commit('clearItems')
const queue = new JobQueue()
splitArray(items, splitCount).forEach(
chunk => queue.addJob(done => {
// 分時(shí)間片提交數(shù)據(jù)
requestAnimationFrame(() => {
commit('addItems', chunk)
done()
})
})
)
await queue.start()
}
我們先通過點(diǎn)擊 Genterate items 按鈕創(chuàng)建 10000 條假數(shù)據(jù),然后分別在開啟和關(guān)閉 Time-slicing 的情況下點(diǎn)擊 Commit items 按鈕提交數(shù)據(jù),開啟 Chrome 的 Performance 面板記錄它們的性能,會(huì)得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們可以發(fā)現(xiàn),優(yōu)化前總的 script 執(zhí)行時(shí)間要比優(yōu)化后的還要少一些,但是從實(shí)際的觀感上看,優(yōu)化前點(diǎn)擊提交按鈕,頁面會(huì)卡死 1.2 秒左右,在優(yōu)化后,頁面不會(huì)完全卡死,但仍然會(huì)有渲染卡頓的感覺。
那么為什么在優(yōu)化前頁面會(huì)卡死呢?因?yàn)橐淮涡蕴峤坏臄?shù)據(jù)過多,內(nèi)部 JS 執(zhí)行時(shí)間過長,阻塞了 UI 線程,導(dǎo)致頁面卡死。
優(yōu)化后,頁面仍有卡頓,是因?yàn)槲覀儾鸱謹(jǐn)?shù)據(jù)的粒度是 1000 條,這種情況下,重新渲染組件仍然有壓力,我們觀察 fps 只有十幾,會(huì)有卡頓感。通常只要讓頁面的 fps 達(dá)到 60,頁面就會(huì)非常流暢,如果我們把數(shù)據(jù)拆分粒度變成 100 條,基本上 fps 能達(dá)到 50 以上,雖然頁面渲染變流暢了,但是完成 10000 條數(shù)據(jù)總的提交時(shí)間還是變長了。
使用 Time slicing技術(shù)可以避免頁面卡死,通常我們?cè)谶@種耗時(shí)任務(wù)處理的時(shí)候會(huì)加一個(gè) loading 效果,在這個(gè)示例中,我們可以開啟 loading animation,然后提交數(shù)據(jù)。對(duì)比發(fā)現(xiàn),優(yōu)化前由于一次性提交數(shù)據(jù)過多,JS 一直長時(shí)間運(yùn)行,阻塞 UI 線程,這個(gè) loading 動(dòng)畫是不會(huì)展示的,而優(yōu)化后,由于我們拆成多個(gè)時(shí)間片去提交數(shù)據(jù),單次 JS 運(yùn)行時(shí)間變短了,這樣 loading 動(dòng)畫就有機(jī)會(huì)展示了。
這里要注意的一點(diǎn),雖然我們拆時(shí)間片使用了
requestAnimationFrameAPI,但是使用requestAnimationFrame本身是不能保證滿幀運(yùn)行的,requestAnimationFrame保證的是在瀏覽器每一次重繪后會(huì)執(zhí)行對(duì)應(yīng)傳入的回調(diào)函數(shù),想要保證滿幀,只能讓 JS 在一個(gè) Tick 內(nèi)的運(yùn)行時(shí)間不超過 17ms。
Non-reactive data
第八個(gè)技巧,使用 Non-reactive data ,你可以查看這個(gè)在線示例。
優(yōu)化前代碼如下:
const data = items.map(
item => ({
id: uid++,
data: item,
vote: 0
})
)
優(yōu)化后代碼如下:
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
}
還是前面的示例,我們先通過點(diǎn)擊 Genterate items 按鈕創(chuàng)建 10000 條假數(shù)據(jù),然后分別在開啟和關(guān)閉 Partial reactivity 的情況下點(diǎn)擊 Commit items 按鈕提交數(shù)據(jù),開啟 Chrome 的 Performance 面板記錄它們的性能,會(huì)得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們可以看到優(yōu)化后執(zhí)行 script 的時(shí)間要明顯少于優(yōu)化前的,因此性能體驗(yàn)更好。
之所以有這種差異,是因?yàn)閮?nèi)部提交的數(shù)據(jù)的時(shí)候,會(huì)默認(rèn)把新提交的數(shù)據(jù)也定義成響應(yīng)式,如果數(shù)據(jù)的子屬性是對(duì)象形式,還會(huì)遞歸讓子屬性也變成響應(yīng)式,因此當(dāng)提交數(shù)據(jù)很多的時(shí)候,這個(gè)過程就變成了一個(gè)耗時(shí)過程。
而優(yōu)化后我們把新提交的數(shù)據(jù)中的對(duì)象屬性 data 手動(dòng)變成了 configurable 為 false,這樣內(nèi)部在 walk 時(shí)通過 Object.keys(obj) 獲取對(duì)象屬性數(shù)組會(huì)忽略 data,也就不會(huì)為 data 這個(gè)屬性 defineReactive,由于 data 指向的是一個(gè)對(duì)象,這樣也就會(huì)減少遞歸響應(yīng)式的邏輯,相當(dāng)于減少了這部分的性能損耗。數(shù)據(jù)量越大,這種優(yōu)化的效果就會(huì)更明顯。
其實(shí)類似這種優(yōu)化的方式還有很多,比如我們?cè)诮M件中定義的一些數(shù)據(jù),也不一定都要在 data 中定義。有些數(shù)據(jù)我們并不是用在模板中,也不需要監(jiān)聽它的變化,只是想在組件的上下文中共享這個(gè)數(shù)據(jù),這個(gè)時(shí)候我們可以僅僅把這個(gè)數(shù)據(jù)掛載到組件實(shí)例 this 上,例如:
export default {
created() {
this.scroll = null
},
mounted() {
this.scroll = new BScroll(this.$el)
}
}
這樣我們就可以在組件上下文中共享 scroll 對(duì)象了,即使它不是一個(gè)響應(yīng)式對(duì)象。
Virtual scrolling
第九個(gè)技巧,使用 Virtual scrolling ,你可以查看這個(gè)在線示例。
優(yōu)化前組件的代碼如下:
<div class="items no-v">
<FetchItemViewFunctional
v-for="item of items"
:key="item.id"
:item="item"
@vote="voteItem(item)"
/>
</div>
優(yōu)化后代碼如下:
<recycle-scroller
class="items"
:items="items"
:item-size="24"
>
<template v-slot="{ item }">
<FetchItemView
:item="item"
@vote="voteItem(item)"
/>
</template>
</recycle-scroller>
還是前面的示例,我們需要開啟 View list,然后點(diǎn)擊 Genterate items 按鈕創(chuàng)建 10000 條假數(shù)據(jù)(注意,線上示例最多只能創(chuàng)建 1000 條數(shù)據(jù),實(shí)際上 1000 條數(shù)據(jù)并不能很好地體現(xiàn)優(yōu)化的效果,所以我修改了源碼的限制,本地運(yùn)行,創(chuàng)建了 10000 條數(shù)據(jù)),然后分別在 Unoptimized 和 RecycleScroller 的情況下點(diǎn)擊 Commit items 按鈕提交數(shù)據(jù),滾動(dòng)頁面,開啟 Chrome 的 Performance 面板記錄它們的性能,會(huì)得到如下結(jié)果。
優(yōu)化前:

優(yōu)化后:

對(duì)比這兩張圖我們發(fā)現(xiàn),在非優(yōu)化的情況下,10000 條數(shù)據(jù)在滾動(dòng)情況下 fps 只有個(gè)位數(shù),在非滾動(dòng)情況下也就十幾,原因是非優(yōu)化場景下渲染的 DOM 太多,渲染本身的壓力很大。優(yōu)化后,即使 10000 條數(shù)據(jù),在滾動(dòng)情況下的 fps 也能有 30 多,在非滾動(dòng)情況下可以達(dá)到 60 滿幀。
之所以有這個(gè)差異,是因?yàn)樘摂M滾動(dòng)的實(shí)現(xiàn)方式:是只渲染視口內(nèi)的 DOM。這樣總共渲染的 DOM 數(shù)量就很少了,自然性能就會(huì)好很多。
虛擬滾動(dòng)組件也是 Guillaume Chau 寫的,感興趣的同學(xué)可以去研究它的源碼實(shí)現(xiàn)。它的基本原理就是監(jiān)聽滾動(dòng)事件,動(dòng)態(tài)更新需要顯示的 DOM 元素,計(jì)算出它們?cè)谝晥D中的位移。
虛擬滾動(dòng)組件也并非沒有成本,因?yàn)樗枰跐L動(dòng)的過程中實(shí)時(shí)去計(jì)算,所以會(huì)有一定的 script 執(zhí)行的成本。因此如果列表的數(shù)據(jù)量不是很大的情況,我們使用普通的滾動(dòng)就足夠了。
總結(jié)
通過這篇文章,我希望你能了解到 Vue.js 的九種性能優(yōu)化技巧,并能運(yùn)用到實(shí)際的開發(fā)項(xiàng)目中。除了上述技巧之外,還有懶加載圖片、懶加載組件、異步組件等等常用的性能優(yōu)化手段。
在做性能優(yōu)化前,我們需要分析性能的瓶頸在哪,才能因地制宜。另外,性能優(yōu)化都需要數(shù)據(jù)支撐的,你在做任何性能優(yōu)化前,需要先采集優(yōu)化前的數(shù)據(jù),這樣優(yōu)化后才能夠通過數(shù)據(jù)對(duì)比看到優(yōu)化的效果。
希望你在日后的開發(fā)過程中,不再只滿足于實(shí)現(xiàn)需求,寫每一行代碼的時(shí)候,都能思考它可能產(chǎn)生的性能方面的影響。
參考資料
[1] vue-9-perf-secrets slidse:https://slides.com/akryum/vueconfus-2019
[2] vue-9-perf-secrets 分享演講視頻:https://www.vuemastery.com/conferences/vueconf-us-2019/9-performance-secrets-revealed/
[3] vue-9-perf-secrets 項(xiàng)目源碼:https://github.com/Akryum/vue-9-perf-secrets
[4] vue-9-perf-secrets 在線演示地址:https://vue-9-perf-secrets.netlify.app/
[5] vue-9-perf-secrets 討論 issue:https://github.com/Akryum/vue-9-perf-secrets/issues/1
[6] vue-virtual-scroller 項(xiàng)目源碼:https://github.com/Akryum/vue-virtual-scroller
到此這篇關(guān)于Vue.js九個(gè)性能優(yōu)化技巧(值得收藏)的文章就介紹到這了,更多相關(guān)Vue.js性能優(yōu)化技巧內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue3后臺(tái)管理系統(tǒng)之創(chuàng)建和配置項(xiàng)目
后臺(tái)管理系統(tǒng)是我們?nèi)粘i_發(fā)學(xué)習(xí)經(jīng)常遇到的一個(gè)項(xiàng)目,下面這篇文章主要給大家介紹了關(guān)于Vue3后臺(tái)管理系統(tǒng)之創(chuàng)建和配置項(xiàng)目的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09
Vue項(xiàng)目頁面跳轉(zhuǎn)時(shí)瀏覽器窗口上方顯示進(jìn)度條功能
這篇文章主要介紹了Vue項(xiàng)目頁面跳轉(zhuǎn)時(shí)瀏覽器窗口上方顯示進(jìn)度條功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03
vuex 中輔助函數(shù)mapGetters的基本用法詳解
mapGetters輔助函數(shù)僅僅是將 store 中的 getter 映射到局部計(jì)算屬性,在組件或界面中不使用mapGetter調(diào)用映射vuex中的getter,在組件或界面中使用mapGetter調(diào)用映射vuex中的getter,具體內(nèi)容跟隨小編一起通過本文學(xué)習(xí)吧2021-07-07
Vue.js 中取得后臺(tái)原生HTML字符串 原樣顯示問題的解決方法
這篇文章主要介紹了VUE.js 中取得后臺(tái)原生HTML字符串 原樣顯示問題 ,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-06-06
vue-devtools?開發(fā)工具插件之支持vue3?chrome?瀏覽器插件
這篇文章主要介紹了vue-devtools?開發(fā)工具插件之支持vue3?chrome?瀏覽器插件,用這個(gè)版本主要是為了支持vue3?推薦直接下載,文中給大家提供了下載地址,感興趣的朋友跟隨小編一起看看吧2022-01-01
對(duì)vue中的input輸入框進(jìn)行郵箱驗(yàn)證方式
這篇文章主要介紹了對(duì)vue中的input輸入框進(jìn)行郵箱驗(yàn)證方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
el-form表單el-form-item label不換行問題及解決
這篇文章主要介紹了el-form表單el-form-item label不換行問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10

