很久没写东西,博客又长草了,这段时间身心放松了很久,都没什么主题能够写了html
上周接到一个需求,优化vue的一个长列表页面,忙活了好久也到尾声了,内存使用和卡顿都作了一点点优化,还算有点收获vue
写的有点啰嗦,能够看一下我是怎么进行这个优化的,也许有点帮助呢node
这个长列表页面,实际上是一个实时日志上报的页面,随着页面打开时间的增长,日志数量也会增多,常规的页面布局和渲染免不了会遇到性能问题。git
使用了vue框架,框架内部的虚拟DOM和组件缓存已经作了一些优化,比起原生实现是有了一些优化处理。github
但这个页面是用到element-ui的el-table组件,渲染出来的是表格数据列表,众所周知,表格在渲染的时候须要绘制整个表格区,因此,web
第一步就是将表格实现改成其余元素标签实现chrome
这一步操做以后,其实没什么大的变化的,几千条日志(每条日志还有不少信息)左右,滚动页面明显卡顿严重element-ui
而需求又改不了,日志能够展开查看详情或收起,已经看过的日志在下次看的时候不须要加载,新的日志会实时添加进来缓存
之前在作大表格数据鼠标滑过行着色的时候,也有严重的卡顿,当时主要的优化手段是不对全部数据进行处理,仅处理视窗可见区域,也能够在这里试试,因此app
第二步就是仅渲染视窗可见的数据
这种方案的原理是使用一个大容器做为滚动区域,里面有一个内容区域,JS经过数据数量和每条数据的高度计算出内容区的高度,内容区用padding或绝对定位撑开滚动区域,让容器可滚动,另外就是数据项了,滚动的时候,计算当前滚动位置scrollTop,再从数据项中找出各项的高度,从头至尾计算出此时容器中放什么数据
哈哈哈 ... 这文字描述简直了,看不懂就不看了吧,能够去看下别人的解说
知道原理以后,实现起来也不难,不过代码就写的比较凌乱了,仍是使用现成的比较成熟的vue插件吧,比较方便
复制粘贴一顿猛操做以后,页面从新展示出来,想着应该能够收工了吧
然鹅,测试的时候发现,页面内存使用能够达到一两G,看来不只要优化卡顿,还要优化内存使用
还能遇到这种少见的页面崩溃,也算是开了眼了
这个方案是把原先页面应该渲染的全部DOM拆分出来,动态地渲染该渲染的部分,
因此就会有一个问题,动态计算须要时间,当滚动很是快的时候会有明显的卡顿现象,因此
第三步就是进行函数节流,即控制scroll事件的处理,在规定的时间内仅触发一次
// 函数节流,频繁操做中间隔 delay 的时间才处理一次 function throttle(fn, delay) { delay = delay || 200; var timer = null; // 每次滚动初始的标识 var timestamp = 0; return function() { var arg = arguments; var now = Date.now(); // 设置开始时间 if (timestamp === 0) { timestamp = now; } clearTimeout(timer); timer = null; // 已经到了delay的一段时间,进行处理 if (now - timestamp >= delay) { fn.apply(this, arg); timestamp = now; } // 添加定时器,确保最后一次的操做也能处理 else { timer = setTimeout(function() { fn.apply(this, arg); // 恢复标识 timestamp = 0; }, delay); } } }; var count = 0; window.onscroll = throttle(function(e) { console.log(e.type, ++count); // scroll }, 500);
虽然改善不是很大,但好歹也是一种方案
接下来是针对这个磨人的内存占用了,也花了蛮多时间去分析去定位,头发又少了几根..
现象是这样的:
刚进入页面的时候,最初100条数据,仅渲染30条数据,内存就占用了100+M
滚动的时候内存蹭蹭蹭往上涨,峰值能到几个G,一段时间后又降低一部分
随着数据总量的增多,内存最初的占用和最后的占用也不一样
在常规滚动和快速滚动的时候,内存占用也不一样
最后发如今数据总量必定的时候,内存最大占用量是固定的(垃圾回收以后)
嗯挺奇怪的,实际项目比较复杂,有其余组件干扰,很差排除法分析
因此就从插件给的Demo 开刀,发现它的表现是一致的
分析要有数据,实验和方案选取要有对比测试
因此使用Chrome DevTool 自带的 Memory工具,另外为了不Chrome插件的影响,在隐身窗口中进行调试
上面有个强制垃圾回收的按钮,JS垃圾回收机制是什么这里就不说了,能够去搜一下
目前垃圾回收方案主要都是标记清除法了,而实现主要是根据GC根往下一层层遍历,遍历不到的对象会被垃圾回收掉,当某些对象本应该被回收,但仍是能从GC根访问的时候,就产生了内存泄漏,主要须要考虑两类内存泄漏:普通JS的对象,游离的DOM节点(本该被回收,却还有对象引用它)
垃圾回收的时间点是不固定的,随机的,咱们在代码中无法控制
点击左边的第一个小圆圈就能够开始分析了,通常来讲分析以前都会自动进行垃圾回收,不过为了更准确,能够再强制点按钮回收一次
经常使用的主要就是两种分析方式:
第一种是进行堆快照(JS的对象通常放在堆中),查看当前的内存分布状况
第二种是进行内存时间线分析,查看一顿操做以后的内存增加状况,主要针对这个操做过程(这个时候能够结合Performance标签功能中来分析)
上图中左侧是两个快照的结果,64.5M是进入页面以后的内存快照,149M是各类操做以后的内存快照
<VirtualList :size="50" :remain="6" :bench="44" class="list" :start="startIndex" :debounce="10"> <Item v-for="(udf, index) of items" :index="index" :key="index"></Item> </VirtualList>
这个长列表总共10w条数据,仅仅渲染了50条(6 + 44)数据,每条数据仅仅是短短的字符串,不应占用这么多内存
去看下内存具体占用状况
内容有点多,由于用的是vue,因此咱们只须要关注比较重要的虚拟DOM对象 VNode和渲染的组件就好了
VNode基本就是全部的数据了,VueComponent是当前渲染的,因此,这里的VNode是否是有不少内存浪费了,与之关联的不少东西也占坑了
看看字符串内容,每条仅仅占用了32字节,因此这里想到的一个点是要缩减Item项的数量
而后,想一想为何全部虚拟DOM都留在了内存中呢,展开一个来看对象的引用关系,有一个$slot.default
而后回去看看插件的实现,插件是将全部子项目都放到了子元素中,以slot的方式插入,而后在内部抽出进行再建立
容器组件在从新渲染的时候,确实能触发了组件的销毁函数 destroy,而这个也将对象间的关系清的干干净净的了
具体能够看vue中组件是怎么销毁的
Vue.prototype.$destroy = function () { var vm = this; if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy'); vm._isBeingDestroyed = true; // remove self from parent var parent = vm.$parent; if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm); } // teardown watchers if (vm._watcher) { vm._watcher.teardown(); } var i = vm._watchers.length; while (i--) { vm._watchers[i].teardown(); } // remove reference from data ob // frozen object may not have observer. if (vm._data.__ob__) { vm._data.__ob__.vmCount--; } // call the last hook... vm._isDestroyed = true; // invoke destroy hooks on current rendered tree vm.__patch__(vm._vnode, null); // fire destroyed hook callHook(vm, 'destroyed'); // turn off all instance listeners. vm.$off(); // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null; } // release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null; } };
把$vnode的对象关系都切的差很少了,但slot方式的使用下是处理不了的,因此在垃圾回收以后,内存中的vnode对象很是多
再来看看内存占用的最大值
能够发现VNode增加了一部分,而最为瞩目的是VueComponent数量居然有那么多,按道理应该只有渲染的几个组件的
为了作对比,咱们通常使用comparison对比两个快照,看看相差的地方
相关使用能够去看文档
有兴趣的也能够导入我这两个快照自行分析 default maximum
这段时间里建立的vue对象基本没能被清理掉,说明有不少不该该出现的对象引用关系,其中detached HTMLDivElement是指游离的DOM对象,通常用于分析DOM相关的内存泄漏,能够猜想出这里的主角应该是vue的组件
挑一个组件来看看,能够发现它仍是和slot有关的,因此滚动期间建立的组件,属于VNode节点的componentInstance属性,而VNode节点无法被回收,因此组件驻留在内存中
接下来的问题是,既然一开始VNode是全部的数据了,为什么在滚动期间,还会有那么多VNode会建立出来
挑一个这期间增长的VNode来看看引用关系,能够发现VNode中有两种,增长的是不一样的_vnode
@后面带的是对象的id,另外咱们也能够在调试的时候,console打印出它们是不一样的对象
通过上面各类分析,有两个问题须要去解决:
减小驻留的VNode和Vue组件
减小操做期间增长的对象
减小驻留,即不用slot的方式,那只能改插件了
插件中vm.$slots.default 获取到的是vnode节点,而后再使用render函数传递vnode进行建立组件并渲染
由此想来,咱们也能够本身建立vnode节点,
不直接写成子组件,而是将纯粹的数据项和组件单元传递给插件,让插件来建立vnode节点
<VirtualList :size="50" :remain="6" :bench="44" class="list" :start="startIndex" :items="items" :item-component="itemComponent" :item-binding="itemBinding"> </VirtualList>
items 是数据项,itemComponent是 import 进来的一个组件单元,itemBinding是一个函数,返回相似渲染函数的data对象,用以传递属性
itemBinding(item, idx) { return { key: item, props: { index: item } }; // return { // key: item.id, // props: { // index: item.num, // }, // nativeOn: { // dblclick: (...args) => { // console.log(idx, 'dblclick'); // } // } // } }
在插件内部,接收传递进来的items和itemComponent,构造出相应的vnodes,固然slots方式也能够支持
for (var i = delta.start; i <= Math.ceil(delta.end); i++) { targets.push(!this.itemComponent ? slots[i] // create vnode, using custom attrs binder : this.$createElement(this.itemComponent, this.itemBinding(this.items[i], i) || {}) ) } return targets
完整的代码实例能够看这里
解决办法挺简单的,虽然这一步建立会耗费一些时间,不过测试发现,跟原先的作法差很少的,原先的也须要建立
来看看优化以后的内存占用状况
一样的数据,最初进入页面占用5M,各类操做以后也差很少,操做之中建立的vue对象基本被清理掉了,且对象数量还算符合预期
在当前10万条简单数据下,内存使用初始减少成1/13,最大减少成1/26,并且随着总数量的增长,优化比率也更高
在实际项目组件复杂的状况下使用,400条日志,内存使用大概由400M到80M,优化率达到了1/5,也挺可观
接下来考虑一下如何减小操做期间增长的对象
这就须要收集一些操做过程当中的数据了
分析过程,我比较喜欢用Performance面板,这里有很是详细的函数调用栈,
另外还要使用调试大法,由最开始的onScroll事件入口开始,一步一步地理解组件建立更新销毁过程,看看哪些地方合不合理,能不能在上游在外部间接地改进
点击左侧小圆圈开始记录,而后滚动一段时间,而后结束记录,查看收集的信息
勾选了右上角的memory选项框知乎,这个面板也能够查看内存的使用,不过记得手动进行一次垃圾回收(那个按钮),由于它通常在记录以前不会自动调用
能够发现仍是比较规律的,挑这段略为明显的进行分析
有兴趣的也能够本身导入我这份数据进行分析
能够发现这里发生了组件的更新,$mount和$destroy的调用,是发生在插件从新渲染可视区域组件的时候
找到关键的地方,调试分析发现每次都会建立新的VNode对象
这样看来,操做期间建立的对象是避免不了的了,只能经过减小操做期间函数执行的次数了,即最初提到的函数节流
而组件销毁的时候,会判断组件是否为keepAlive型,能够尝试一下给Item组件加上,这能解决操做期间组件建立和销毁带来的内存开销,不过会致使全部组件都会驻留在内存中,综合考虑下,这种方案不可取
最后想一想,再挤出一点优化方案,既然操做过程当中会建立组件,而组件里可能还有子组件,因此,还能够优化子组件
即Item组件内部,能不用组件的能够不用组件,改成普通HTMl标签代替,通过测试,确实能改善那么一丢丢
一个性能问题的排查分析和解决,文章略长略啰嗦,到这里就结束了
总结一下,主要的五个优化
1. 将表格实现改成其余元素标签实现
2. 仅渲染视窗可见的数据
3. 进行函数节流
4. 减小驻留的VNode和Vue组件,不使用显示的子组件slot方式,改成手动建立虚拟DOM来切断对象引用
5. 减小操做期间增长的对象,操做时组件必然会更新建立,能够减小组件中子组件的数量