前言 又到了我最喜欢的水博客时间,今天让我们来看看在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; var canMove = !removeOnly; if (process.env.NODE_ENV !== 'production' ) { checkDuplicateKeys(newCh); } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx]; } 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)) { 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)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); if (isUndef(idxInOld)) { 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 { 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
就会不处理这个对象,唯一的缺点是你不能操作这个对象了,每次修改都只能重新生成一个
使用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
访问到这个外部仓库了
使用函数式组件 对于一些简单的展示用组件可以使用函数式组件来减少性能开销
不过仅限于Vue2
使用非实时绑定的表单项 事实上,因为v-model
是v-bind
和input
事件的语法糖,所以每次在输入框里打一个字符,整个组件就要重新进行render
,dom-diff
等一系列流程,所以就有可能有下面的问题
耗费性能
有可能导致动画卡顿(因为JS执行线程和浏览器渲染线程是互斥的)
所以,我们可以给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-for
和v-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时就渲染出了页面
然后是不延迟加载
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
时才渲染出页面
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; }, }, }; }
这个技术其实不能让页面的总加载时间变少,反而会变多,不过嘛,用户一般会觉得,你可以动的慢,但你不能一点都不动长时间卡死,所以也算是一种优化
组件懒加载 如果你用过图片懒加载,那接下来的东西就很好理解了,原理基本一致
首先加载组件的骨架屏
然后在页面滚动到差不多要看到组件的位置时,再真正加载组件
判断的方法可以使用监听onscoll
和onresize
事件或者使用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' , component: () => import ( '../views/About.vue' ) } ] const router = new VueRouter({ mode: 'history' , base: process.env.BASE_URL, routes }) export default router
简单来说,除了首屏渲染的路由组件,其他组件都可以用动态加载
动态加载的好处可以看我这篇文章:Webpack性能优化
简单来说就是分了个包,按需加载,这样刚刚打开页面时传输的东西就少了,渲染就快了
后记 没想到啥了,想到再补充吧