vue渲染大量数据优化_记一次vue长列表的内存性能分析和优化

vue渲染⼤量数据优化_记⼀次vue长列表的内存性能分析和优
好久没写东西,博客⼜长草了,这段时间⾝⼼放松了好久,都没什么主题可以写了
上周接到⼀个需求,优化vue的⼀个长列表页⾯,忙活了很久也到尾声了,内存使⽤和卡顿都做了⼀点点优化,还算有点收获
写的有点啰嗦,可以看⼀下我是怎么进⾏这个优化的,也许有点帮助呢
这个长列表页⾯,其实是⼀个实时⽇志上报的页⾯,随着页⾯打开时间的增加,⽇志数量也会增多,常规的页⾯布局和渲染免不了会遇到性能问题。
使⽤了vue框架,框架内部的虚拟DOM和组件缓存已经做了⼀些优化,⽐起原⽣实现是有了⼀些优化处理。
但这个页⾯是⽤到element-ui的el-table组件,渲染出来的是表格数据列表,众所周知,表格在渲染的时候需要绘制整个表格区,所以,
第⼀步就是将表格实现改为其他元素标签实现
这⼀步操作之后,其实没什么⼤的变化的,⼏千条⽇志(每条⽇志还有很多信息)左右,滚动页⾯明显卡顿严重
⽽需求⼜改不了,⽇志可以展开查看详情或收起,已经看过的⽇志在下次看的时候不需要加载,新的⽇志会实时添加进来
以前在做⼤表格数据⿏标滑过⾏着⾊的时候,也有严重的卡顿,当时主要的优化⼿段是不对所有数据进⾏处理,仅处理视窗可见区域,也可以在这⾥试试,所以
第⼆步就是仅渲染视窗可见的数据
这种⽅案的原理是使⽤⼀个⼤容器作为滚动区域,⾥⾯有⼀个内容区域,JS通过数据数量和每条数据的⾼度计算出内容区的⾼度,内容区⽤padding或绝对定位撑开滚动区域,让容器可滚动,另外就是数据项了,滚动的时候,计算当前滚动位置scrollTop,再从数据项中出各项的⾼度,从头到尾计算出此时容器中放什么数据
哈哈哈 ... 这⽂字描述简直了,看不懂就不看了吧,可以去看下别⼈的解说
知道原理之后,实现起来也不难,不过代码就写的⽐较凌乱了,还是使⽤现成的⽐较成熟的vue插件吧,⽐较⽅便
复制粘贴⼀顿猛操作之后,页⾯重新展现出来,想着应该可以收⼯了吧
然鹅,测试的时候发现,页⾯内存使⽤可以达到⼀两G,看来不仅要优化卡顿,还要优化内存使⽤
还能遇到这种少见的页⾯崩溃,也算是开了眼了
这个⽅案是把原先页⾯应该渲染的所有DOM拆分出来,动态地渲染该渲染的部分,
所以就会有⼀个问题,动态计算需要时间,当滚动⾮常快的时候会有明显的卡顿现象,所以
第三步就是进⾏函数节流,即控制scroll事件的处理,在规定的时间内仅触发⼀次
//函数节流,频繁操作中间隔 delay 的时间才处理⼀次
functionthrottle(fn, delay) {
delay= delay || 200;var timer = null;//每次滚动初始的标识
var timestamp = 0;return function() {var arg =arguments;var now =w();//设置开始时间
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);
}csmate
}芯片封装
};var count = 0;
window.οnscrοll= throttle(function(e) {
console.pe,++count); //scroll
}, 500);
代码参考
虽然改善不是很⼤,但好⽍也是⼀种⽅案
接下来是针对这个磨⼈的内存占⽤了,也花了蛮多时间去分析去定位,头发⼜少了⼏根..
现象是这样的:
刚进⼊页⾯的时候,最初100条数据,仅渲染30条数据,内存就占⽤了100+M
滚动的时候内存蹭蹭蹭往上涨,峰值能到⼏个G,⼀段时间后⼜下降⼀部分
随着数据总量的增多,内存最初的占⽤和最后的占⽤也不同
在常规滚动和快速滚动的时候,内存占⽤也不同
最后发现在数据总量⼀定的时候,内存最⼤占⽤量是固定的(垃圾回收之后)
嗯挺奇怪的,实际项⽬⽐较复杂,有其他组件⼲扰,不好排除法分析
所以就从插件给的Demo 开⼑,发现它的表现是⼀致的
分析要有数据,实验和⽅案选取要有对⽐测试
所以使⽤Chrome DevTool ⾃带的 Memory⼯具,另外为了避免Chrome插件的影响,在隐⾝窗⼝中进⾏调试
上⾯有个强制垃圾回收的按钮,JS垃圾回收机制是什么这⾥就不说了,可以去搜⼀下
⽬前垃圾回收⽅案主要都是标记清除法了,⽽实现主要是根据GC根往下⼀层层遍历,遍历不到的对象会被垃圾回收掉,当某些对象本应该被回收,但还是能从GC根访问的时候,就产⽣了内存泄漏,主要需要考虑两类内存泄漏:普通JS的对象,游离的DOM节点(本该被回收,却还有对象引⽤它)
垃圾回收的时间点是不固定的,随机的,我们在代码中没法控制
点击左边的第⼀个⼩圆圈就可以开始分析了,⼀般来说分析之前都会⾃动进⾏垃圾回收,不过为了更准确,可以再强制点按钮回收⼀次
常⽤的主要就是两种分析⽅式:
第⼀种是进⾏堆快照(JS的对象⼀般放在堆中),查看当前的内存分布情况
第⼆种是进⾏内存时间线分析,查看⼀顿操作之后的内存增长情况,主要针对这个操作过程(这个时候可以结合Performance标签功能中来分析)
上图中左侧是两个快照的结果,64.5M是进⼊页⾯之后的内存快照,149M是各种操作之后的内存快照
这个长列表总共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._ardown();
}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
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节点
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 <= d); 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事件⼊⼝开始,⼀步⼀步地理解组件创建更新销毁过程,看看哪些地⽅合不合理,能不能在上游在外部间接地改进
点击左侧⼩圆圈开始记录,然后滚动⼀段时间,然后结束记录,查看收集的信息

本文发布于:2024-09-24 00:16:52,感谢您对本站的认可!

本文链接:https://www.17tex.com/tex/2/212140.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:内存   渲染   组件   对象   滚动
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议