如何高效操作 DOM 元素?( 二 )


改变窗口大小
与之相反的操作则只会引起重绘,比如:
设置背景图片
修改字体颜色
改变 visibility 属性值
如果想了解更多关于重绘和重排的样式属性,可以参看这个网址:https://csstriggers.com/ 。
下面是两段验证代码,我们通过 Chrome 提供的性能分析工具来对渲染耗时进行分析 。
第一段代码,通过修改 div 元素的边距来触发重排,渲染耗时(粗略地认为渲染耗时为紫色 Rendering 事件和绿色 Painting 事件耗时之和)3045 毫秒 。
const times = 100000let html = ''for(let i=0;i<times;i++) {html+= `<div>${i}</div>`}document.body.innerHTML += htmlconst divs = document.querySelectorAll('div')Array.prototype.forEach.call(divs, (div, i) => {div.style.margin = i % 2 ? '10px' : 0;})第二段代码,修改 div 元素字体颜色来触发重绘,得到渲染耗时 2359 ms 。
const times = 100000let html = ''for(let i=0;i<times;i++) {html+= `<div>${i}</div>`}document.body.innerHTML += htmlconst divs = document.querySelectorAll('div')Array.prototype.forEach.call(divs, (div, i) => {div.style.color = i % 2 ? 'red' : 'green';})从两段测试代码中可以看出,重排渲染耗时明显高于重绘,同时两者的 Painting 事件耗时接近,也应证了重排会导致重绘 。
如何高效操作 DOM
明白了 DOM 操作耗时之处后,要提升性能就变得很简单了,反其道而行之,减少这些操作即可 。
在循环外操作元素
比如下面两段测试代码对比了读取 1000 次 JSON 对象以及访问 1000 次 body 元素的耗时差异,相差一个数量级 。
const times = 10000;console.time('switch')for (let i = 0; i < times; i++) {document.body === 1 ? console.log(1) : void 0;}console.timeEnd('switch') // 1.873046875msvar body = JSON.stringify(document.body)console.time('batch')for (let i = 0; i < times; i++) {body === 1 ? console.log(1) : void 0;}console.timeEnd('batch') // 0.846923828125ms当然即使在循环外也要尽量减少操作元素,因为不知道他人调用你的代码时是否处于循环中 。
批量操作元素
比如说要创建 1 万个 div 元素,在循环中直接创建再添加到父元素上耗时会非常多 。如果采用字符串拼接的形式,先将 1 万个 div 元素的 html 字符串拼接成一个完整字符串,然后赋值给 body 元素的 innerHTML 属性就可以明显减少耗时 。
const times = 10000;console.time('createElement')for (let i = 0; i < times; i++) {const div = document.createElement('div')document.body.AppendChild(div)}console.timeEnd('createElement')// 54.964111328125msconsole.time('innerHTML')let html=''for (let i = 0; i < times; i++) {html+='<div></div>'}document.body.innerHTML += html // 31.919921875msconsole.timeEnd('innerHTML')虽然通过修改 innerHTML 来实现批量操作的方式效率很高,但它并不是万能的 。比如要在此基础上实现事件监听就会略微麻烦,只能通过事件代理或者重新选取元素再进行单独绑定 。批量操作除了用在创建元素外也可以用于修改元素属性样式,比如下面的例子 。
创建 2 万个 div 元素,以单节点树结构进行排布,每个元素有一个对应的序号作为文本内容 。现在通过 style 属性对第 1 个 div 元素进行 2 万次样式调整 。下面是直接操作 style 属性的代码:
const times = 20000;let html = ''for (let i = 0; i < times; i++) {html = `<div>${i}${html}</div>`}document.body.innerHTML += htmlconst 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'}如果将需要修改的样式属性放入 JavaScript 数组,然后对这些修改进行 reduce 操作,得到最终需要的样式之后再设置元素属性,那么性能会提升很多 。代码如下:
const times = 20000;let html = ''for (let i = 0; i < times; i++) {html = `<div>${i}${html}</div>`}document.body.innerHTML += htmllet queue = [] // 创建缓存样式的数组let microTask // 执行修改样式的微任务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}const setStyle = (style) => {queue.push(style)// 创建微任务if(!microTask) microTask = Promise.resolve().then(st)}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)}从下面的耗时占比图可以看到,紫色 Rendering 事件耗时有所减少 。


推荐阅读