Vue 初始化性能优化

原文: https://github.com/Coffcer/Bl...javascript

前言

通常来讲,你不须要太关心vue的运行时性能,它在运行时很是快,但付出的代价是初始化时相对较慢。在最近开发的一个Hybrid APP里,Android Webview初始化一个较重的vue页面居然用了1200ms ~ 1400ms,这让我开始重视vue的初始化性能,并最终优化到200 ~ 300ms,这篇文章分享个人优化思路。html

性能瓶颈在哪里?

先看一下常见的vue写法:在html里放一个app组件,app组件里又引用了其余的子组件,造成一棵以app为根节点的组件树。vue

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

而正是这种作法引起了性能问题,要初始化一个父组件,必然须要先初始化它的子组件,而子组件又有它本身的子组件。那么要初始化根标签<app>,就须要从底层开始冒泡,将页面全部组件都初始化完。因此咱们的页面会在全部组件都初始化完才开始显示。java

这个结果显然不是咱们要的,更好的结果是页面能够从上到下按顺序流式渲染,这样可能整体时间增加了,但首屏时间缩减,在用户看来,页面打开速度就更快了。git

要实现这种渲染模式,我总结了下有3种方式实现。第3种方式是我认为最合适的,也是我在项目中实际使用的优化方法。github

第一种:不使用根组件

这种方式很是简单,例如:数组

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

抛弃了根组件<app>,从而使A、B、C每个组件初始化完都马上展现。但根组件在SPA里是很是必要的,因此这种方式只适用小型页面。浏览器

第二种:异步组件

异步组件在官方文档已有说明,使用很是简单:app

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

这里<B>组件是一个异步组件,会等到手动调用resolve函数时才开始初始化,而父组件<app>也没必要等待<B>先初始化完。dom

咱们利用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 )
    }
})

看起来很美好,但这种方式也有问题,考虑下这样的结构:

<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-ifv-for这种是terminal指令,会在指令内部编译组件。若是你想要本身实现一个terminal指令,须要加上terminal: true,例如:

Vue.directive('lazy', {
    terminal: true,
    bind () {},
    update () {},
    unbind () {}
});

这是vue在1.0.19+新增的功能,因为比较冷门,文档也没有特别详细的叙述,最好的方式是参照着v-ifv-for的源码来写。

我已经为此封装了一个terminal指令,你能够直接使用:
https://github.com/Coffcer/vu...

其余的优化点

除了组件上的优化,咱们还能够对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 }
        ]);
    }
})

后记

vue 1.0+ 的组件其实不算轻量,初始化一个组件包括依赖收集、转换等过程,但其实有些是能够放在编译时提早完成的。vue 2.0+ 已经在这方面作了很多的改进:分离了编译时和运行时、提供函数组件等,能够预见,vue 2.0的性能将有很大的提高。

v-lazy-component: https://github.com/Coffcer/vu...

相关文章
相关标签/搜索