# 前言

众所周知,Vue 项目采用了数据双向绑定和虚拟 DOM 基础,在数据驱动代替 DOM 频繁渲染已经算是非常高效了,对开发者而言已经非常优化了,那为什么还会有 Vue 性能优化这一说呢?

因为目前 Vue 2.x 使用了 webpack 等第三方打包构建工具,并且支持其他第三方的插件,我们在项目中使用这些工具时可能不同的操作在运行或打包效率上会有不同的效果,下面就来详细说明优化的方向。

# v-if 和 v-show 的使用

  • v-iffalse 的时候不会渲染 DOM 到视图,为 true 的时候才会渲染到视图;
  • v-show 不管初始条件是什么,元素总是会渲染到视图,只是简单地基于 CSS 的 display 属性进行切换。

最佳实践:频繁切换显示隐藏的元素采用 v-show ,很少改变使用 v-if

# computed 和 watch 区分使用

  • computed : 是计算属性,依赖其它属性值,并且 computed 的值有 缓存 ,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
  • watch : 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;

最佳实践:当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

# v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

现在不加 key 一般会报错的,添加 key 可以方便 Vue 内部机制精准找到该条列表数据。当更新时,新的状态值和旧的状态值对比,较快地定位到 diff

v-forv-if 优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成 computed 属性。

<ul>
   <li v-for="user in adminUsers" :key="user.id">
       
   </li>
</ul>
<script>
export default {
	data () {
		return { users: [] }
	},
	computed: {
		adminUsers: function(){
			return this.users.filter(()=>user.isAdmin)
		}
	}
}
</script>

# 纯显示长列表性能优化

对于只用来展示用的数据,不需要做 vue 做数据劫持,只需要冻结这个对象即可:

export default {
	data () {
		return {
			users: []
		}
	},
	created () {
		axios.get('/api/users').then((res)=>{
			this.users = Object.freeze(res.data.users)
		})
	}
}

# 事件的销毁

Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListenesetInterval 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:

created() {
    addEventListener('click', this.click, false)
    this.timer = setInterval(this, refresh, 2000)
},
beforeDestroy() {
    removeEventListener('click', this.click, false)
    clearInterval(this.timer)
}

# 图片资源懒加载

使用 vue-lazyload 插件:
安装

npm install vue-lazyload --save-dev

man.js 引用

import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload)
// 或自定义
Vue.use(VueLazyload, {
	preLoad: 1.3,
	error: 'dist/error.png',
	loading: 'dist/loading.gif',
	attempt: 1
})

修改 img 标签

<img v-lazy="/static/img/1.png">

# keep-alive 缓存页面 / 组件

一个内置组件,可以再多个组件间动态切换时缓存被移除的组件实例

在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染 DOM,减少加载时间及性能消耗,提高用户体验性。

# 路由懒加载

Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来。

推荐对所有的路由都使用动态导入

// 将
// import Foo from './Foo'
// 替换成
const Foo = () => import('./Foo.vue')
const router = new VueRouter({
	routes: [
		{ path: '/foo', component: Foo }
	]
})

# 第三方插件按需引入

我们在使用第三方库的时候,最好是按需引入而不是全局引入,因为第三方库的插件比较多全部引入会打包比较慢,如 Element UIAnt Design of Vue 等 UI 库:

按需引入

import Vue from 'vue';
import { DatePicker } from 'ant-design-vue';
Vue.use(DatePicker);

全局引入

import Antd from 'ant-design-vue';
Vue.use(Antd);

# 优化无限列表性能

如果你是在渲染带无限滚动加载的列表时,那么需要采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-listvue-virtual-scroller 来优化这种无限列表的场景的。
大家自己去 Github 看使用说明吧。

# 函数式组件

在 Vue 2.x 中,函数式组件的初始化速度比有状态组件快的多,所以可以将无状态的组件标记为函数式组件

// Vue 2.x 函数式组件创建
export default {
    functional: true, // 标记
    props: ['level'],
    render(h, { props, data, children }) {
        return h(`h${props.level}`, data, children)
    }
}

备注:无状态也就是没有响应式数据,也没有实例(没有 this 上下文)

但是在 Vue 3.x 中, Vue 2.x 带来的函数式组件的性能提升可以忽略不计,所以函数式组件的作用也没那么大了,可以用作简单的组件封装。

# 子组件拆分

优化前:

<template>
    <div :style="{ opacity: number / 300 }">
        <div><!--swig1--></div>
	</div>
</template>
<script>
export default {
	props: ['number'],
	methods: {
        heavy () { 
            //... 耗时任务
        }
    }
}   
</script>

优化后:

<template>
    <div :style="{ opacity: number / 300 }">
        <ChildComp/>
    </div>
</template>
<script>
export default {
	components: {
        ChildComp: {
            methods: {
                heavy () { ... },
            },
            render (h) {
                return h('div', this.heavy())
            }
        }
    },
    props: ['number']
}
</script>

优化后的方式是把这个耗时任务 heavy 函数的执行逻辑用子组件 ChildComp 封装了

由于 Vue 的更新是组件粒度的,虽然每一帧都通过数据修改导致了父组件的重新渲染,但是 ChildComp 却不会重新渲染,因为它的内部也没有任何响应式数据的变化。

所以优化后的组件不会在每次渲染都执行耗时任务,自然执行的 JavaScript 时间就变少了。

# 变量本地化

<script>
export default {
	props: ['start'],
	computed: {
        base () {
            return 42
        },
        result () {
            const base = this.base // 不要频繁引用 this.base
            let result = this.start
            for (let i = 0; i < 1000; i++) {
                result += heavy(base)
            }
            return result
        }
    }
}
</script>

每次访问 this.base 的时候,由于 this.base 是一个响应式对象,所以会触发它的 getter,进而会执行依赖收集相关逻辑代码。

从需求上来说, this.base 执行一次依赖收集就够了,把它的 getter 求值结果返回给局部变量 base,进行缓存,后续再次访问 base 的时候就不会触发 gatter,也不会走依赖收集的逻辑了,性能自然就得到了提升。

# 服务端渲染 SSR or 预渲染

一般单页应用是在浏览器端完成页面渲染的,数据是发请求从后台拿过来的;而服务器端渲染 SSR 是页面元素的结构 (HTML) 是在服务器端就已经构建好的,直接把整个页面返回到客户端的。
那 SSR 有什么优缺点呢:

  • 更好的 SEO :网络爬虫可以直接爬取页面信息利于被搜索引擎收录,而 ajax 异步请求的内容不会被收录,所以通过 SSR 渲染的完整的页面信息更利于 SEO;

# 服务端渲染 SSR or 预渲染

一般单页应用是在浏览器端完成页面渲染的,数据是发请求从后台拿过来的;而服务器端渲染 SSR 是页面元素的结构 (HTML) 是在服务器端就已经构建好的,直接把整个页面返回到客户端的。
那 SSR 有什么优缺点呢:

  • 支持的钩子函数只支持 beforCreatecreated ,服务器需要处于 Node Server 环境;
  • 需要更高的服务器配置:因为它包含了数据处理和页面渲染,所以服务器开支变大

如果对首屏加载速度要求比较高或对 SEO 有要求的可以采用 SSR 渲染。