前言

又到了我最喜欢的水博客时间,今天让我们来看看在vue项目中的一些性能优化小技巧吧

这个应该会长期更新,啥时候看到了新的优化方法就可以丢上来

代码优化

在v-for中使用key

key的使用有两个好处

  • diff算法中的就地转化变成移动元素
  • 防止input元素出现一些状态错误的bug(这个Vue3修复了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
var oldStartIdx = 0;
var newStartIdx = 0;
var oldEndIdx = oldCh.length - 1;
var oldStartVnode = oldCh[0];
var oldEndVnode = oldCh[oldEndIdx];
var newEndIdx = newCh.length - 1;
var newStartVnode = newCh[0];
var newEndVnode = newCh[newEndIdx];
var oldKeyToIdx, idxInOld, vnodeToMove, refElm;

// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
var canMove = !removeOnly;

if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh);
}

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (isUndef(oldKeyToIdx)) {
// 生成map<key, node>
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 看看能否找到对应的节点
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
if (isUndef(idxInOld)) { // New element
// 找不到直接转化
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
} else {
// 找到进行移动
vnodeToMove = oldCh[idxInOld];
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined;
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
// 添加新增节点
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else if (newStartIdx > newEndIdx) {
// 移除多余节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}

处理不需要观测的数据

有一些数据我们不需要观测,比如下面这些

  • 纯展示数据
  • 一些用于计算的数据

这时候就不要把它们放到data里了,试想一想,你有一个一万项的对象数组,结果它只是拿来算数的,结果Vue全部弄了观测,那对性能影响多大(不过Vue3使用了Proxy和对深层对象懒代理,减少了这个影响消耗的性能)

解决办法有下面几种

使用Object.freeze冻结对象,Vue就会不处理这个对象,唯一的缺点是你不能操作这个对象了,每次修改都只能重新生成一个

image-20210321132505198

使用computed返回一个对象,因为这个对象有缓存,所以会一直返回同一个对象,而且这个对象也不会被观测,缺点和上面一样,不能对这个对象进行修改

1
2
3
4
5
computed : {
qaq() {
return [Math.random()]
}
}

或者把数据独立出来放到外面去维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let weakMap = new WeakMap();
class ComponentStore {
getStore() {
return weakMap.get(this);
}

setStore(store) {
return weakMap.set(this, store);
}
}


let componentStore = new ComponentStore();

export {componentStore};
1
2
import {componentStore} from "./util/ComponentStore";
Vue.prototype.$componentStore = componentStore;

然后就可以在组件中通过this.$componentStore访问到这个外部仓库了

使用函数式组件

对于一些简单的展示用组件可以使用函数式组件来减少性能开销

image-20210321134121313

不过仅限于Vue2

image-20210321134243986

使用非实时绑定的表单项

事实上,因为v-modelv-bindinput事件的语法糖,所以每次在输入框里打一个字符,整个组件就要重新进行renderdom-diff等一系列流程,所以就有可能有下面的问题

  • 耗费性能
  • 有可能导致动画卡顿(因为JS执行线程和浏览器渲染线程是互斥的)

image-20210321135622800

所以,我们可以给v-model加个修饰符,变成v-model.lazy,这样就可以解决页面频繁渲染的问题

记得解绑事件

对我这种event bus玩家,这一点还是很重要的,不然整个虚拟DOM(而且虚拟DOM上保存了真实DOM的引用)都没法销毁了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div class="ProjectFooterBarView wrapper">
<div v-show="hasPrev" class="prev" @click="handleClick('prev')"> < 上一页 </div>
<div class="save" @click="handleClick('save')"> 保存 </div>
<div class="next" @click="handleClick('next')"> {{hasNext ? '下一页' : '完成'}} > </div>
</div>
</template>

<script lang="ts">
import {Component, Prop, Vue, Watch} from "vue-property-decorator";

@Component({})
export default class ProjectFooterBar extends Vue {
mounted() {
this.$bus.$on("prevPage", this.prevPage);
this.$bus.$on("nextPage", this.nextPage);
}
beforeDestroy() {
this.$bus.$off("prevPage", this.prevPage);
this.$bus.$off("nextPage", this.nextPage);
}

}
</script>

<style scoped lang="scss">
@import "ProjectFooterBar";
</style>

使用v-show代替v-if

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

比如弹窗,最好是使用v-show而不是v-if

延迟加载组件

在首屏如果要渲染大量的组件,就会导致长时间的白屏,这是因为Vue会先生成整个虚拟DOM树,然后全部渲染到页面上,而我们就可以先加载一部分的DOM,举个例子

有这样一个渲染了很多元素的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div class="item-container">
<div class="item" v-for="n in 5000"></div>
</div>
</template>

<script>
export default {};
</script>

<style scoped>
.item-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
}

.item {
width: 5px;
height: 3px;
background: #ccc;
margin: 0.1em;
}
</style>

分别测试延迟加载和不延迟加载的情况

首先是延迟加载(这里为了方便才用的v-forv-if混写,平时不要这么用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div class="container">
<div class="block" v-for="n in 21" v-if="defer(n)">
<heavy-comp></heavy-comp>
</div>
</div>
</template>

<script>
import HeavyComp from "./HeavyComp.vue";
import defer from "../mixin/defer";

export default {
mixins: [defer(21)],
components: {HeavyComp},
};
</script>

<style scoped>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
}

.block {
border: 3px solid #f40;
}
</style>

可以看到JS执行和渲染是轮流进行的,而且在900ms时就渲染出了页面

image-20210321154637746

然后是不延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
<div class="container">
<div class="block" v-for="n in 21">
<heavy-comp></heavy-comp>
</div>
</div>
</template>

<script>
import HeavyComp from "./HeavyComp.vue";

export default {
components: {HeavyComp},
};
</script>

<style scoped>
.container {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 1em;
}

.block {
border: 3px solid #f40;
}
</style>

可以看到页面有一大段时间都被JS和计算样式阻塞了,2000ms时才渲染出页面

image-20210321154945178

defer是这个东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 让组件在多少帧之后才参与渲染
export default function (maxFrameCount) {
return {
data() {
return {
frameCount: 0,
};
},
mounted() {
const refreshFrameCount = () => {
requestAnimationFrame(() => {
this.frameCount++;
if (this.frameCount < maxFrameCount) {
refreshFrameCount();
}
});
};
refreshFrameCount();
},
methods: {
defer(showInFrameCount) {
return this.frameCount >= showInFrameCount;
},
},
};
}

这个技术其实不能让页面的总加载时间变少,反而会变多,不过嘛,用户一般会觉得,你可以动的慢,但你不能一点都不动长时间卡死,所以也算是一种优化

组件懒加载

如果你用过图片懒加载,那接下来的东西就很好理解了,原理基本一致

首先加载组件的骨架屏

img

然后在页面滚动到差不多要看到组件的位置时,再真正加载组件

判断的方法可以使用监听onscollonresize事件或者使用IntersectionObserver API

也可以直接使用第三方库:点我QvQ

使用keep-alive组件缓存页面

keep-alive可以缓存生成的虚拟DOM和真实DOM,在下次用到的时候就可以直接把它们插回页面

应该都用过吧…..也不知道怎么介绍,反正就是用空间换时间的操作

虚拟列表

如果要做一个很长长长长的列表,就不要直接渲染了,可以使用虚拟列表

虚拟列表简单来说,就是只渲染固定数量的DOM,然后在滚动时动态计算要展示哪些项,获取到项对应的数据后把它们渲染到DOM里去

当然也是有库的:vue-virtual-scroller

orz我记得还有一个国内作者写的,忘了叫啥了,找到了再丢上来

传输优化

组件路由使用动态加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// 懒加载
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]

const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})

export default router

简单来说,除了首屏渲染的路由组件,其他组件都可以用动态加载

动态加载的好处可以看我这篇文章:Webpack性能优化

简单来说就是分了个包,按需加载,这样刚刚打开页面时传输的东西就少了,渲染就快了

后记

没想到啥了,想到再补充吧