前言
一般來說,你不需要太關心vue的運行時性能,它在運行時非常快,但付出的代價是初始化時相對較慢。在最近開發的一個Hybrid APP裏,Android Webview初始化一個較重的vue頁面竟然用了1200ms ~ 1400ms,這讓我開始重視vue的初始化性能,並最終優化到200 ~ 300ms,這篇文章分享我的優化思路。
性能瓶頸在哪裏?
先看一下常見的vue寫法:在html裏放一個app組件,app組件裏又引用了其他的子組件,形成一棵以app爲根節點的組件樹。
而正是這種做法引發了性能問題,要初始化一個父組件,必然需要先初始化它的子組件,而子組件又有它自己的子組件。那麼要初始化根標籤,就需要從底層開始冒泡,將頁面所有組件都初始化完。所以我們的頁面會在所有組件都初始化完纔開始顯示。
這個結果顯然不是我們要的,更好的結果是頁面可以從上到下按順序流式渲染,這樣可能總體時間增長了,但首屏時間縮減,在用戶看來,頁面打開速度就更快了。
要實現這種渲染模式,我總結了下有3種方式實現。第3種方式是我認爲最合適的,也是我在項目中實際使用的優化方法。
第一種:不使用根組件
這種方式非常簡單,例如:
拋棄了根組件,從而使A、B、C每一個組件初始化完都立刻展示。但根組件在SPA裏是非常必要的,所以這種方式只適用小型頁面。
第二種:異步組件
異步組件在官方文檔已有說明,使用非常簡單:
new Vue({
components: {
A: { /*component-config*/ },
B (resolve) {
setTimeout(() => {
resolve({ /*component-config*/ })
}, 0);
}
}
})
這裏組件是一個異步組件,會等到手動調用resolve函數時纔開始初始化,而父組件也不必等待先初始化完。
我們利用setTimeout(fn, 0)將的初始化放在隊列最後,結果就是頁面會在初始化完後立刻顯示,然後再顯示。如果你的頁面有幾十個組件,那麼把非首屏的組件全設成異步組件,頁面顯示速度會有明顯的提升。
你可以封裝一個簡單的函數來簡化這個過程:
function deferLoad (component, time = 0) {
return (resolve) => {
window.setTimeout(() => resolve(component), time)
};
}
new Vue({
components: {
B: deferLoad( /*component-config*/ ),
// 100ms後渲染
C: deferLoad( /*component-config*/, 100 )
}
})
看起來很美好,但這種方式也有問題,考慮下這樣的結構:
還是按照上面的異步組件做法,這時候就需要考慮把哪些組件設成異步的了。如果把A、B、C都設成異步的,那結果就是3個會首先渲染出來,頁面渲染的過程在用戶看來非常奇怪,並不是預期中的從上到下順序渲染。
第三種:v-if 和 terminal指令
這是我推薦的一種做法,簡單有效。還是那個結構,我們給要延遲渲染的組件加上v-if:
new Vue({
data: {
showB: false,
showC: false
},
created () {
// 顯示B
setTimeout(() => {
this.showB = true;
}, 0);
// 顯示C
setTimeout(() => {
this.showC = true;
}, 0);
}
});
這個示例寫起來略顯囉嗦,但它已經實現了我們想要的順序渲染的效果。頁面會在A組件初始化完後顯示,然後再按順序渲染其餘的組件,整個頁面渲染方式看起來是流式的。
有些人可能會擔心v-if
存在一個編譯/卸載過程,會有性能影響。但這裏並不需要擔心,因爲v-if
是惰性的,只有當第一次值爲true時纔會開始初始化。
這種寫法看起來很麻煩,如果我們能實現一個類似v-if
的組件,然後直接指定多少秒後渲染,那就更好了,例如:
一個簡單的指令即可,不需要js端任何配合,並且可以用在普通dom上面,Nice!
在vue裏,類似v-if
和v-for
這種是terminal指令,會在指令內部編譯組件。如果你想要自己實現一個terminal指令,需要加上terminal: true
,例如:
Vue.directive('lazy', {
terminal: true,
bind () {},
update () {},
unbind () {}
});
這是vue在1.0.19+新增的功能,由於比較冷門,文檔也沒有特別詳細的敘述,最好的方式是參照着v-if
和v-for
的源碼來寫。
我已經爲此封裝了一個terminal指令,你可以直接使用:
github.com/Coffcer/vue…
其他的優化點
除了組件上的優化,我們還可以對vue的依賴改造入手。初始化時,vue會對data做getter、setter改造,在現代瀏覽器裏,這個過程實際上挺快的,但仍然有優化空間。
Object.freeze()
是ES5新增的API,用來凍結一個對象,禁止對象被修改。vue 1.0.18+以後,不會對已凍結的data做getter、setter轉換。
如果你確保某個data不需要跟蹤依賴,可以使用Object.freeze將其凍結。但請注意,被凍結的是對象的值,你仍然可以將引用整個替換調。看下面例子:
{{ item.value }}
new Vue({
data: {
// vue不會對list裏的object做getter、setter綁定
list: Object.freeze([
{ value: 1 },
{ value: 2 }
])
},
created () {
// 界面不會有響應
this.list[0].value = 100;
// 下面兩種做法,界面都會響應
this.list = [
{ value: 100 },
{ value: 200 }
];
this.list = Object.freeze([
{ value: 100 },
{ value: 200 }
]);
}
})
原文章鏈接地址:https://juejin.im/entry/57d8f0840bd1d00057d10e7c#comment