Vue2常见优化手段

永远不要过早优化,要做到因地制宜,见招拆招。

Vue 应用运行时性能优化措施

1. 使用 key

对于通过循环生成了列表,应该给每个列表项一个稳定且唯一的 key,这有利于在列表变动时,尽量少的删除、新增、改动元素。

2. 使用冻结对象

冻结的对象不会被响应化。有的时候没有必要将数据变成响应式的数据,因为在数据的响应化的过程中需要递归遍历数据,需要耗时。

Object.freeze(this.data)

如果不希望数据被 Observer,其实可以在 created 的生命周期中把数据挂在到 this 上,并不一定都在 data、props 或者是 computed 中定义。另外,这部分数据也是可以被修改的,只是他们的变化不会触发组件重新渲染,因为我们也并不希望,这样比用 Object.freeze 更加灵活。

3. 使用函数式组件

参见 函数式组件

使用函数式组件,在 js 执行时间以及渲染时间上稍有减少,但是差别不大,同时在内存占用(消耗)方面会比普通组件占用少,这是因为使用函数式组件不会在 vue 的组件树中生成该组件,不会为函数组件创建实例,只是纯渲染。对于普通组件,vue 内部会通过 new VueComponent()对每一个组件创建新的组件实例

函数式组件无状态 (没有响应式数据),也没有实例 (没有 this 上下文)

4. 使用计算属性

如果模板中某个数据会使用多次,并且该数据是通过计算得到的,使用计算属性以缓存它们

5. 非实时绑定的表单项

当使用v-model绑定一个表单项时,当用户改变表单项的状态时,也会随之改变数据,从而导致 vue 发生重渲染(rerender),这会带来一些性能的开销。
特别是当用户改变表单项时,页面有一些动画正在进行中,由于 JS 执行线程和浏览器渲染线程是互斥的,最终会导致动画出现卡顿。
我们可以通过使用 lazy 或不使用 v-model 的方式解决该问题,但要注意,这样可能会导致在某一个时间段内数据和表单项的值是不一致的。

6. 保持对象引用稳定

在绝大部分情况下,vue 触发 rerender 的时机是其依赖的数据发生变化,若数据没有发生变化,哪怕给数据重新赋值了,vue 也是不会做出任何处理的,下面是 vue 判断数据没有变化的源码

export function hasChanged(x:unknown,y:unknown):boolean {
if(x === y) {
return x === 0 && 1/x !=== 1 / (y as number) // +0 === -0 --> true
} else {
return x === x || y === y // NaN !== NaN --> true
}
}

因此,如果需要,只要能保证组件的依赖数据不发生变化,组件就不会重新渲染。

对于原始数据类型,保持其值不变即可

对于对象类型,保持其引用不变即可

从另一方面来说,由于可以通过保持属性引用稳定来避免子组件的重渲染,那么我们应该细分组件来尽量避免多余的渲染

7. 使用 v-show 替代 v-if

对于频繁切换显示状态的元素,使用 v-show 可以保证虚拟 dom 树的稳定,避免频繁的新增和删除元素,特别是对于那些内部包含大量 dom 元素的节点,这一点极其重要

关键字:频繁切换显示状态、内部包含大量 dom 元素

8. 使用延迟装载(defer)

首页白屏时间主要受到两个因素的影响:

  • 打包体积过大
    巨型包需要消耗大量的传输时间,导致 JS 传输完成前页面只有一个<div>,没有可显示的内容

  • 需要立即渲染的内容太多
    JS 传输完成后,浏览器开始执行 JS 构造页面。
    但可能一开始要渲染的组件太多,不仅 JS 执行的时间很长,而且执行完后浏览器要渲染的元素过多,从而导致页面白屏

一个可行的办法就是延迟装载组件,让组件按照指定的先后顺序依次一个一个渲染出来

延迟装载是一个思路,本质是利用 requestAnimationFrame 事件分批渲染内容,它的具体实现多种多样。

9. 使用 keep-alive

keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。被包裹的组件在切换的时候不会被销毁,直接使用缓存中的实例,好处是避免创建组件带来的开销、而且可以保留组件的状态,但是会因此占用更多的内存。

属性

  • include 哪些组件缓存
  • exclude 哪些组件不缓存
  • max 最大缓存数,超出 vue 会自动移除最久没有使用的组件缓存
// 写法1
<keep-alive :include="['Comp1', 'Comp2']">
<component :is="comps[curIndex]">
</keep-alive>
// 写法2
<keep-alive include="Comp1,Comp2" :max="2">
<component :is="comps[curIndex]">
</keep-alive>

关于 max 属性,vue 是这么判断的

cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: _getComponentName(componentOptions),
tag,
componentInstance
}
keys.push(keyToCache)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode) // 移除keys中的第一个
}
this.vnodeToCache = null
}
}

特有的生命周期

  • activated 组件激活时调用,第一次调用是在 mounted 之后调用
  • deactivated 组件失活时调用

常见的应用场景

后台管理系统的选项卡,当菜单添加到选项卡时,就对选项卡中的菜单进行缓存,提高用户的使用体验。

原理

keep-alive 在内部维护一个 keys 数组和一个缓存对象 cache

created() {
this.cache = Object.create(null)
this.keys = []
},

keys 数组记录缓存组件的 key 值,如果组件没有,就会为组件自动生成一个唯一的 key 值

const { cache, keys } = this
const key =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key

cache 以 key 值为键,以 vnode 为值,用于缓存组件对应的虚拟 DOM

keys:[1,2,3]
cache:{
1:vnode1,
2,vnode2,
3:vnode3
}

大致流程:

render() {
const slot = this.$slots.default // 获取默认插槽
const vnode = getFirstComponentChild(slot) // 得到插槽中第一个组件的vnode
const componentOptions = vnode && vnode.componentOptions // 获取vnode的信息
if (componentOptions) {
// check pattern
const name = _getComponentName(componentOptions) // 获取组件名字
const { include, exclude } = this
// 判断是否需要缓存
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 来到这里,说明组件需要缓存,有缓存复用组件实例,无缓存则进行缓存
const { cache, keys } = this
// 获取组件的key
const key =
vnode.key == null
? componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
} else {
// 无缓存,进行缓存
this.vnodeToCache = vnode
this.keyToCache = key
}
// @ts-expect-error can vnode.data can be undefined
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}

10.长列表优化

如果你的应用存在非常长或者无限滚动的列表,那么采用 窗口化 的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。
vue-virtual-scroll-listvue-virtual-scroller 都是解决这类问题的开源项目。你也可以参考 Google 工程师的文章 Complexities of an Infinite Scroller 来尝试自己实现一个虚拟的滚动列表来优化性能,主要使用到的技术是 DOM 回收、墓碑元素和滚动锚定。

Vue 应用加载性能优化措施

1.利用服务端渲染(SSR)和预渲染(Prerender)来优化加载性能

在一个单页应用中,往往只有一个 html 文件,然后根据访问的 url 来匹配对应的路由脚本,动态地渲染页面内容。单页应用比较大的问题是首屏可见时间过长。
单页面应用显示一个页面会发送多次请求,第一次拿到 html 资源,然后通过请求再去拿数据,再将数据渲染到页面上。而且由于现在微服务架构的存在,还有可能发出多次数据请求才能将网页渲染出来,每次数据请求都会产生 RTT(往返时延),会导致加载页面的时间拖的很长。

Vue2 SSR 指南
优化向:单页应用多路由预渲染指南

2.组件懒加载

在超长应用内容的场景中,通过组件懒加载方案可以优化初始渲染的运行性能,其实,这对于优化应用的加载性能也很有帮助。
组件粒度的懒加载结合异步组件和 webpack 代码分片,可以保证按需加载组件,以及组件依赖的资源、接口请求等,比起通常单纯的对图片进行懒加载,更进一步的做到了按需加载资源。

使用组件懒加载方案对于超长内容的应用初始化渲染很有帮助,可以减少大量必要的资源请求,缩短渲染关键路径。