这是我的第一篇博客,一起参与掘金新人创作活动,开启写作之路。
输出内容才能更好的理解输入的知识,正好借助这次新人活动 记录这次优化
背景
日志查看、指令展示 等 列表很长,且不能使用分页、触底懒加载的场景
使用 vue+element-ui 表格展示接口返回数据,数据量为万级。
页面内容白屏二十秒左右后显示出数据,期间页面其他功能/按钮无法正常的交互,之后页面滚动、点击等事件也十分卡顿,用户体验较差。
最开始的数据量接近 10w,分类型后数据变少了渲染也变快了,但还是要为大数据量情况做准备
排查页面性能消耗
一、性能概览
通过 dev tools 查看 performance 渲染分析
通过概要 可以发现主要耗时是 scripting 和 Rendring
二、内存消耗
打印内存快照进行对比,其实在 performance 可以看到 Js Heap 和 Nodes 发生了明显的增加
#Delta(内存回收差值) = #New (新分配内存) - #Deleted (销毁内存)
再去 Memory 打印 内存快照(take heap snapshot) 跟正常页面内存快照进行 对比(Comparison) #Delta 可以发现:
- InternalNode (内置节点)、Array、Closure(闭包) 有明显提升
- 其实 listener 也有明显提升 但在写文之前已经优化过了
el-table 可能会在每行表格的元素里监听一些 dom 事件增加 listener 优化:采用原生 table 标签(因为只需要做展示)
解决问题
分析原因
通过 performance 的分析,可以发现长时间的 Js 任务阻塞了渲染。
为什么 JS 任务会阻塞渲染?
实际是因为浏览器的 GUI 渲染线程和 JavaScript 引擎线程之间的互斥,JavaScript 在执行期间会阻塞 UI 的渲染,如果脚本执行时间太长会由于页面长时间无响应而崩溃。
为什么页面事件会没有响应?
用户的操作会触发事件触发线程,当事件被触发后,会把事件添加到任务队列的最尾端,等待 js 引擎处理执行
Event Loop
执行代码的JS 引擎线程是单线程的,同一时间只能做一件事情。
浏览器上发生很多事件,比如页面渲染、文件读取、脚本执行、网络请求等等。
但并不是所有事件的回调都是立即执行的,为了协调这些事件的处理顺序,浏览器使用异步任务回调通知模式
Js 代码会分为 同步任务 和 异步任务 ,同步任务直接放在 Js 引擎线程 上执行,形成一个 执行栈。
异步任务分别进入 异步 http 请求线程 或 定时器触发线程。当符合一定条件时(如定时器到时,请求响应)就会把回调加入任务队列。
执行栈上的同步任务完成后,Js 线程空闲,系统会读取任务队列,将其中的异步任务回调添加到执行栈中。
为什么渲染会耗时这么久?
而 Js 执行完成后 生成了 大量 DOM,GUI 线程需要一次性将它们渲染到页面上,要解析 HTML、CSS 构成 DOMTree 和 RenderTree,然后 Layout、Paint 将消耗大量时间(效果表现解析并为 丢帧 和 长时间空白)
优化方式
如果我们可以把 Js 任务 拆分成多个片段,每个片段只解析一定量的数据,将整个流程控制为一边解析数据一边渲染。
那用户就能立刻看到页面,并且在没有明显渲染感觉的情况下正常使用页面
但如何衡量每个片段应该分配多少 执行时间 ,解析多少 数据量, 何时执行渲染 呢
关于 浏览器渲染 和 requestAnimationFrame
浏览器渲染
如图所示,浏览器一帧里包含:页面事件-->执行脚本代码(宏任务+微任务)-->执行rAF-->渲染(样式计算,布局,重绘)-->执行 rIC
不一定每一轮 event loop 都会对应一次浏览 器渲染,要根据屏幕刷新率、页面性能、页面是否在后台运行来共同决定,通常来说这个渲染间隔是固定的。
浏览器会尽可能的保持帧率稳定,例如页面性能无法维持 60fps(每 16.66ms 渲染一次)的话,那么浏览器就会选择 30fps 的更新速率,而不是偶尔丢帧。
如果浏览器上下文不可见,那么页面会降低到 4fps 左右甚至更低。 如果满足以下条件,也会跳过渲染:
- 浏览器判断更新渲染不会带来视觉上的改变。
- map of animation frame callbacks 为空,也就是帧动画回调为空,可以通过 requestAnimationFrame 来请求帧动画。
rIC (requestIdelCallback),rIC 不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事 且还有剩余时间,才会执行。
长时间的事件和任务会 阻塞渲染,导致页面空白。
Event Loop 中,当 JS 引擎的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
requestAnimationFrame
requestAnimationFrame 在重新渲染之前执行,使用一个回调函数作为参数,可以在浏览器进行下一个渲染前执行回调。
浏览器页面刷新频率一般与设备保持一致,当页面每秒绘制的帧数(FPS)达到 60 时,人眼才会觉得流畅
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行—— MDN window.requestAnimationFrame
实现代码
我们利用rAF将Js代码执行平均分配在每一帧的渲染
总体上页面渲染耗时变长了,但用户是立即看到页面和数据,也可以像访问正常页面一样去操作页面
总结
因为 JS 的 Event Loop 机制,JS 引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
通过 requestAnimationFrame 我们可以由浏览器来决定回调函数的执行时机,并将大量数据的多次渲染分为多个片段,在每个片段中解析定量数据交给浏览器渲染,第一时间将页面展现给用户。
虽然问题看起来解决了,但其实遗留的缺点并不少:
页面渲染效率,页面性能,DOM 节点多、结构复杂...
- 时间分片相当于代码替用户去触发懒加载,DOM 是逐次渲染的,渲染消耗的总时间肯定比一次渲染所有 DOM 要慢不少
- 因为页面是逐渐渲染的,如果直接把滚动条拖到底部看到的并不是最后的数据,需要等待渲染完成
- 实际开发出的代码不是一个<tr> or <li>标签加数据绑定这么简单,随着 dom 结构的复杂度(事件监听、样式、子节点...)和 dom 数量的增加,占用的内存也会更多,不可避免的影响页面性能。
第一次写文章,深刻体会到写作不易,无论是文字的表达亦或内容的排版,即使可能没人看在写的时候依然小心翼翼(感觉还是写的很烂,但我已经尽力了 躺)
才疏学浅,如果文章有什么问题欢迎大家指教,后续有时间再写一篇虚拟列表的文章