Android Webview中Vue初始化的性能優(yōu)化
前言
一般來(lái)說(shuō),你不需要太關(guān)心vue的運(yùn)行時(shí)性能,它在運(yùn)行時(shí)非???,但付出的代價(jià)是初始化時(shí)相對(duì)較慢。在最近開(kāi)發(fā)的一個(gè)Hybrid APP里,Android Webview初始化一個(gè)較重的vue頁(yè)面竟然用了1200ms ~ 1400ms,這讓我開(kāi)始重視vue的初始化性能,并最終優(yōu)化到200 ~ 300ms,這篇文章分享我的優(yōu)化思路。
性能瓶頸在哪里?
先看一下常見(jiàn)的vue寫(xiě)法:在html里放一個(gè)app組件,app組件里又引用了其他的子組件,形成一棵以app為根節(jié)點(diǎn)的組件樹(shù)。
- <body>
- <app></app>
- </body>
而正是這種做法引發(fā)了性能問(wèn)題,要初始化一個(gè)父組件,必然需要先初始化它的子組件,而子組件又有它自己的子組件。那么要初始化根標(biāo)簽<app>,就需要從底層開(kāi)始冒泡,將頁(yè)面所有組件都初始化完。所以我們的頁(yè)面會(huì)在所有組件都初始化完才開(kāi)始顯示。
這個(gè)結(jié)果顯然不是我們要的,更好的結(jié)果是頁(yè)面可以從上到下按順序流式渲染,這樣可能總體時(shí)間增長(zhǎng)了,但首屏?xí)r間縮減,在用戶看來(lái),頁(yè)面打開(kāi)速度就更快了。
要實(shí)現(xiàn)這種渲染模式,我總結(jié)了下有3種方式實(shí)現(xiàn)。第3種方式是我認(rèn)為最合適的,也是我在項(xiàng)目中實(shí)際使用的優(yōu)化方法。
***種:不使用根組件
這種方式非常簡(jiǎn)單,例如:
- <body>
- <A></A>
- <B></B>
- <C></C>
- </body>
拋棄了根組件<app>,從而使A、B、C每一個(gè)組件初始化完都立刻展示。但根組件在SPA里是非常必要的,所以這種方式只適用小型頁(yè)面。
第二種:異步組件
異步組件在官方文檔已有說(shuō)明,使用非常簡(jiǎn)單:
- <app>
- <A></A>
- <B></B>
- </app>
- new Vue({
- components: {
- A: { /*component-config*/ },
- B (resolve) {
- setTimeout(() => {
- resolve({ /*component-config*/ })
- }, 0);
- }
- }
- })
這里<B>組件是一個(gè)異步組件,會(huì)等到手動(dòng)調(diào)用resolve函數(shù)時(shí)才開(kāi)始初始化,而父組件<app>也不必等待<B>先初始化完。
我們利用setTimeout(fn, 0)將<B>的初始化放在隊(duì)列***,結(jié)果就是頁(yè)面會(huì)在<A>初始化完后立刻顯示,然后再顯示<B>。如果你的頁(yè)面有幾十個(gè)組件,那么把非首屏的組件全設(shè)成異步組件,頁(yè)面顯示速度會(huì)有明顯的提升。
你可以封裝一個(gè)簡(jiǎn)單的函數(shù)來(lái)簡(jiǎn)化這個(gè)過(guò)程:
- 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 )
- }
- })
看起來(lái)很美好,但這種方式也有問(wèn)題,考慮下這樣的結(jié)構(gòu):
- <app>
- <title></title>
- <A></A>
- <title></title>
- <B></B>
- <title></title>
- <C></C>
- </app>
還是按照上面的異步組件做法,這時(shí)候就需要考慮把哪些組件設(shè)成異步的了。如果把A、B、C都設(shè)成異步的,那結(jié)果就是3個(gè)<title>會(huì)首先渲染出來(lái),頁(yè)面渲染的過(guò)程在用戶看來(lái)非常奇怪,并不是預(yù)期中的從上到下順序渲染。
第三種:v-if 和 terminal指令
這是我推薦的一種做法,簡(jiǎn)單有效。還是那個(gè)結(jié)構(gòu),我們給要延遲渲染的組件加上v-if:
- <app>
- <A></A>
- <B v-if="showB"></B>
- <C v-if="showC"></C>
- </app>
- new Vue({
- data: {
- showB: false,
- showC: false
- },
- created () {
- // 顯示B
- setTimeout(() => {
- this.showB = true;
- }, 0);
- // 顯示C
- setTimeout(() => {
- this.showC = true;
- }, 0);
- }
- });
這個(gè)示例寫(xiě)起來(lái)略顯啰嗦,但它已經(jīng)實(shí)現(xiàn)了我們想要的順序渲染的效果。頁(yè)面會(huì)在A組件初始化完后顯示,然后再按順序渲染其余的組件,整個(gè)頁(yè)面渲染方式看起來(lái)是流式的。
有些人可能會(huì)擔(dān)心v-if存在一個(gè)編譯/卸載過(guò)程,會(huì)有性能影響。但這里并不需要擔(dān)心,因?yàn)関-if是惰性的,只有當(dāng)***次值為true時(shí)才會(huì)開(kāi)始初始化。
這種寫(xiě)法看起來(lái)很麻煩,如果我們能實(shí)現(xiàn)一個(gè)類似v-if的組件,然后直接指定多少秒后渲染,那就更好了,例如:
- <app>
- <A></A>
- <B v-lazy="0"></B>
- <C v-lazy="100"></C>
- </app>
一個(gè)簡(jiǎn)單的指令即可,不需要js端任何配合,并且可以用在普通dom上面,Nice!
在vue里,類似v-if和v-for這種是terminal指令,會(huì)在指令內(nèi)部編譯組件。如果你想要自己實(shí)現(xiàn)一個(gè)terminal指令,需要加上terminal: true,例如:
- Vue.directive('lazy', {
- terminal: true,
- bind () {},
- update () {},
- unbind () {}
- });
這是vue在1.0.19+新增的功能,由于比較冷門(mén),文檔也沒(méi)有特別詳細(xì)的敘述,***的方式是參照著v-if和v-for的源碼來(lái)寫(xiě)。
我已經(jīng)為此封裝了一個(gè)terminal指令,你可以直接使用:
https://github.com/Coffcer/vu...
其他的優(yōu)化點(diǎn)
除了組件上的優(yōu)化,我們還可以對(duì)vue的依賴改造入手。初始化時(shí),vue會(huì)對(duì)data做getter、setter改造,在現(xiàn)代瀏覽器里,這個(gè)過(guò)程實(shí)際上挺快的,但仍然有優(yōu)化空間。
Object.freeze()是ES5新增的API,用來(lái)凍結(jié)一個(gè)對(duì)象,禁止對(duì)象被修改。vue 1.0.18+以后,不會(huì)對(duì)已凍結(jié)的data做getter、setter轉(zhuǎn)換。
如果你確保某個(gè)data不需要跟蹤依賴,可以使用Object.freeze將其凍結(jié)。但請(qǐng)注意,被凍結(jié)的是對(duì)象的值,你仍然可以將引用整個(gè)替換調(diào)??聪旅胬樱?/p>
- <p v-for="item in list">{{ item.value }}</p>
- new Vue({
- data: {
- // vue不會(huì)對(duì)list里的object做getter、setter綁定
- list: Object.freeze([
- { value: 1 },
- { value: 2 }
- ])
- },
- created () {
- // 界面不會(huì)有響應(yīng)
- this.list[0].value = 100;
- // 下面兩種做法,界面都會(huì)響應(yīng)
- this.list = [
- { value: 100 },
- { value: 200 }
- ];
- this.list = Object.freeze([
- { value: 100 },
- { value: 200 }
- ]);
- }
- })
后記
vue 1.0+ 的組件其實(shí)不算輕量,初始化一個(gè)組件包括依賴收集、轉(zhuǎn)換等過(guò)程,但其實(shí)有些是可以放在編譯時(shí)提前完成的。vue 2.0+ 已經(jīng)在這方面做了不少的改進(jìn):分離了編譯時(shí)和運(yùn)行時(shí)、提供函數(shù)組件等,可以預(yù)見(jiàn),vue 2.0的性能將有很大的提升。