最近的项目成果发现总是会在一段时间后崩溃,定位到原因,是一个定时器那块。
起初以为是定时器导致页面崩溃,后来发现是定时器中大量的dom操作导致的页面崩溃。
总结心得:在进行页面编写时,应该尽量避免大规模的dom操作,尽量少地小规模地更改dom结构。
以上,我认为我需要深入了解一下dom操作频率与页面性能的联系。
如何高效操作DOM元素?
Ques:为什么dom操作耗时?
About 浏览器的工作机制:
【1】线程切换
浏览器的两个引擎:JavaScript引擎和渲染引擎 —— 具有互斥性(不能同时工作),由此产生了 线程切换(上下文切换),这一操作相对而言是比较耗时的,频繁大量的线程切换就会造成性能损耗。
【2】重新渲染
重排(Reflow)与 重绘(RePaint)
浏览器在渲染页面时,解析操作:
HTML ——> DOM树;CSS ——> CSSOM树;
DOM操作中涉及到元素、样式的修改会引起渲染引擎重新计算样式,生成新的CSSOM树。
可能会影响到其他元素排布的操作就会引起 重排,继而引发 重绘,比如:
- 修改元素边距、大小;
- 添加、删除元素;
- 改变窗口大小;
引起 重绘 的操作:
- 设置背景图片;
- 改变字体颜色;
- 改变visibility属性值;
for more:https://csstriggers.com/
经测试可知,重排渲染耗时明显高于重绘 。
得出结论:
(1)在循环外操作DOM & 批量操作DOM
DOM操作时最好是先循环拼接dom的字符串,再挂到页面节点上(innerHtml()),避免多次页面重排、重绘;
虽然通过修改 innerHTML 来实现批量操作的方式效率很高,但它并不是万能的。比如要在此基础上实现事件监听就会略微麻烦,只能通过事件代理或者重新选取元素再进行单独绑定。
批量操作除了用在创建元素外也可以用于修改元素属性样式。如果将需要修改的样式属性放入 JavaScript 数组,然后对这些修改进行 reduce 操作,得到最终需要的样式之后再设置元素属性,那么性能会提升很多。(设置元素属性是完成一次任务,多次设置则是看做完成多个任务,完成n次任务会进行n次渲染,造成多余时耗,参见我之前的文章,详解js单线程及异步原理&js事件循环机制:https://blog.csdn.net/ganle/article/details/106669083)
const times = 20000;
let html = ''
for (let i = 0; i < times; i++) {
html = `<div>${i}${html}</div>`
}
document.body.innerHTML += html
const div = document.querySelector('div')
for (let i = 0; i < times; i++) {
div.style.fontSize = (i % 12) + 12 + 'px'
div.style.color = i % 2 ? 'red' : 'green'
div.style.margin = (i % 12) + 12 + 'px'
}
改为:
const times = 20000;
let html = ''
for (let i = 0; i < times; i++) {
html = `<div>${i}${html}</div>`
}
document.body.innerHTML += html
let queue = [] // 创建缓存样式的数组
let microTask // 执行修改样式的微任务
for (let i = 0; i < times; i++) {
const style = {
fontSize: (i % 12) + 12 + 'px',
color: i % 2 ? 'red' : 'green',
margin: (i % 12) + 12 + 'px'
}
setStyle(style);
}
const setStyle = (style) => {
queue.push(style);
// 创建微任务
if(!microTask) microTask = Promise.resolve().then(st);
}
const st = () => {
const div = document.querySelector('div');
// 合并样式
const style = queue.reduce((acc, cur) => ({...acc, ...cur}), {});
for(let prop in style) {
div.style[prop] = style[prop]
}
queue = [];
microTask = null;
}
代码修改后,可以减少渲染耗时;virtualDOM 之所以号称高性能,其实现原理就与此类似。
(2)缓存元素集合
比如将通过选择器函数获取到的 DOM 元素赋值给变量,之后通过变量操作而不是再次使用选择器函数来获取。
because:减少对DOM数的遍历操作,能明显提升性能!
(3)尽量不要使用复杂的匹配规则和复杂的样式,从而减少渲染引擎计算样式规则生成 CSSOM 树的时间;
(4)尽量减少重排和重绘影响的区域;
(5)使用 CSS3 特性来实现动画效果。
(6)利用绝对定位脱离文档流,这样操作定位里面的内容不会引起外部的重排;
(7)开启子线程处理复杂计算,待计算结束后通知主线程进行dom操作;
(8)有动画的话,也可以考虑分层渲染的机制。加上will-change(参考:https://developer.mozilla.org/zh-CN/docs/Web/CSS/will-change)
更多关于Web前端的性能优化问题可参考:
https://www.jianshu.com/p/3a1ee54d9496