Vue 初始化性能優化

ChristinaHu 8年前發布 | 8K 次閱讀 性能優化 JavaScript開發

前言

一般來說,你不需要太關心vue的運行時性能,它在運行時非常快,但付出的代價是初始化時相對較慢。在最近開發的一個Hybrid APP里,Android Webview初始化一個較重的vue頁面竟然用了1200ms - 1400ms,這讓我開始重視vue的初始化性能,并最終優化到200 - 300ms,這篇文章分享我的優化思路。

性能瓶頸在哪里?

先看一下常見的vue寫法:在html里放一個app組件,app組件里又引用了其他的子組件,形成一棵以app為根節點的組件樹。

<body>
    <app></app> 
</body>

而正是這種做法引發了性能問題,要初始化一個父組件,必然需要先初始化它的子組件,而子組件又有它自己的子組件。那么要初始化根標簽 <app> ,就需要從底層開始冒泡,將頁面所有組件都初始化完。所以我們的頁面會在所有組件都初始化完才開始顯示。

這個結果顯然不是我們要的,更好的結果是頁面可以從上到下按順序流式渲染,這樣可能總體時間增長了,但首屏時間縮減,在用戶看來,頁面打開速度就更快了。

要實現這種渲染模式,我總結了下有3種方式實現。第3種方式是我認為最合適的,也是我在項目中實際使用的優化方法。

第一種:不使用根組件

這種方式非常簡單,例如:

<body>
    <A></A>
    <B></B>
    <C></C>
</body>

拋棄了根組件 <app> ,從而使A、B、C每一個組件初始化完都立刻展示。但根組件在SPA里是非常必要的,所以這種方式只適用小型頁面。

第二種:異步組件

異步組件在官方文檔已有說明,使用非常簡單:

<app>
    <A></A>
    <B></B>
</app>
new Vue({
    components: {
        A: { /*component-config*/ },
        B (resolve) {
            setTimeout(() => {
                resolve({ /*component-config*/ })
            }, 0);
        }
    }
})

這里 <B> 組件是一個異步組件,會等到手動調用resolve函數時才開始初始化,而父組件 <app> 也不必等待 <B> 先初始化完。

我們利用setTimeout(fn, 0)將 <B> 的初始化放在隊列最后,結果就是頁面會在 <A> 初始化完后立刻顯示,然后再顯示 <B> 。如果你的頁面有幾十個組件,那么把非首屏的組件全設成異步組件,頁面顯示速度會有明顯的提升。

你可以封裝一個簡單的函數來簡化這個過程:

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 ) } })</code></pre>

看起來很美好,但這種方式也有問題,考慮下這樣的結構:

<app>
    <title></title>
    <A></A>
    <title></title>
    <B></B>
    <title></title>
    <C></C>
</app>

還是按照上面的異步組件做法,這時候就需要考慮把哪些組件設成異步的了。如果把A、B、C都設成異步的,那結果就是3個 <title> 會首先渲染出來,頁面渲染的過程在用戶看來非常奇怪,并不是預期中的從上到下順序渲染。

第三種:v-if 和 terminal指令

這是我推薦的一種做法,簡單有效。還是那個結構,我們給要延遲渲染的組件加上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);
    }
});

這個示例寫起來略顯啰嗦,但它已經實現了我們想要的順序渲染的效果。頁面會在A組件初始化完后顯示,然后再按順序渲染其余的組件,整個頁面渲染方式看起來是流式的。

有些人可能會擔心 v-if 存在一個編譯/卸載過程,會有性能影響。但這里并不需要擔心,因為 v-if 是惰性的,只有當第一次值為true時才會開始初始化。

這種寫法看起來很麻煩,如果我們能實現一個類似 v-if 的組件,然后直接指定多少秒后渲染,那就更好了,例如:

<app>
    <A></A>
    <B v-lazy="0"></B>
    <C v-lazy="100"></C>
</app>

一個簡單的指令即可,不需要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 的源碼來寫。

其他的優化點

除了組件上的優化,我們還可以對vue的依賴改造入手。初始化時,vue會對data做getter、setter改造,在現代瀏覽器里,這個過程實際上挺快的,但仍然有優化空間。

Object.freeze() 是ES5新增的API,用來凍結一個對象,禁止對象被修改。vue 1.0.18+以后,不會對已凍結的data做getter、setter轉換。

如果你確保某個data不需要跟蹤依賴,可以使用Object.freeze將其凍結。但請注意,被凍結的是對象的值,你仍然可以將引用整個替換調。看下面例子:

<p v-for="item in list">{{ item.value }}</p>
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 }
    ]);
}

})</code></pre>

后記

vue 1.0+ 的組件其實不算輕量,初始化一個組件包括依賴收集、轉換等過程,但其實有些是可以放在編譯時提前完成的。vue 2.0+ 已經在這方面做了不少的改進:分離了編譯時和運行時、提供函數組件等,可以預見,vue 2.0的性能將有很大的提升。

 

 

來自:https://segmentfault.com/a/1190000006893428

 

 本文由用戶 ChristinaHu 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!