谈谈DOM性能问题及优化

372 阅读6分钟

前言

DOM即文档对象模型(document object module),是HTML和XML文档的编程接口,可以通过JS进行操作。

浏览器在渲染大规模的DOM节点的时候,会造成大量的浏览器性能损耗,从而产生前端性能问题。

本文我们就来聊聊DOM性能问题及其优化策略。

1. 为什么是DOM

标准的xml/html的文本解析协议,常见的有DOMSAX。在解析速度及内存占用上,SAXDOM有优势,但为什么浏览器选择DOM解析html

1.1 DOM vs SAX

SAX提供一次性解析文本,不生成对象,Iterator模式访问元素,event-basedPUSH模式触发,简单说:应用需要向Parser注册,当Parser遍历xml时,触发调用应用 。

这里说个题外话,改进版StAXPULL模式,但这都不重要了,重要是:一次性文本解析,不生成对象

DOM解析文本后,生成DOM树。即:一次性文本解析,生成对象

1.2 浏览器选择了DOM

单次效率DOM不如SAX,但SAX不生成对象,浏览器很多操作很难满足,比如:元素定位,元素样式渲染……所以DOM是必然之选。

2. DOM的性能问题

2.1 核心问题:重绘(Repaint) 和 重排(Reflow)

当解析的html文件很大时,生成DOM树占用内存较大,同时遍历(不更新)元素耗时也更长。

但这都不是重点,DOM的核心问题是:DOM修改导致的页面重绘、重新排版!重新排版是用户阻塞的操作,同时,如果频繁重排,CPU 使用率也会猛涨!

操作DOM是非常耗性能的,所以我们不仅要避免去操作DOM,还要减少访问DOM的次数。

DOM操作会导致一系列的重绘(repaint)、重新排版(reflow)操作。为了确保执行结果的准确性,所有的修改操作是按顺序同步执行的。大部分浏览器都不会在JavaScript的执行过程中更新DOM。相应的,这些浏览器将对DOM的操作放进一个队列,并在JavaScript脚本执行完毕以后按顺序一次执行完毕。也就是说,在JavaScript执行的过程,直到发生重新排版,用户一直被阻塞。

一般的浏览器中(不含IE),重绘(repaint)的速度远快于重排(reflow),所以避免重排(reflow)更重要

把DOM和JavaScript各自想象成一个岛屿,它们之间用收费桥梁连接。

--《高性能JavaScript》

2.1.1 什么是重绘与重排

重绘

当渲染树render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为重排(reflow)。

每个页面至少需要一次重排,就是在页面第一次加载的时候,这时候是一定会发生重排的,因为要构建render tree

在重排的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成重排后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘

简单来说,回流就是计算元素在设备内的确切位置和大小并且重新绘制

回流的代价要远大于重绘。并且回流必然会造成重绘,但重绘不一定会造成回流。

重排

当渲染树render tree中的一些元素需要更新样式,但这些样式属性只是改变元素的外观,风格,而不会影响布局的,比如background-color,称为重绘(repaint)。

简单来说,重绘就是将渲染树节点转换为屏幕上的实际像素,不涉及重新布局阶段的位置与大小计算。

2.2 会导致重绘或重排的操作

  • 页面首次渲染(无法避免且开销最大的一次)
  • 元素字体大小变化(font-size)
  • 元素内容变化(文字数量或图片大小等等)
  • 激活CSS伪类(例如::hover)
  • DOM元素的添加、修改(内容)、删除( Reflow + Repaint)
  • 仅修改DOM元素的字体颜色(只有Repaint,因为不需要调整布局)
  • 应用新的样式或者修改任何影响元素外观的属性
  • Resize浏览器窗口发生变化、滚动页面
  • 操作元素的某些属性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE))
  • 在JQuery中,CSS选择器,如:var el = $('.hyddd');由于IE6、7不支持,所以Javascript框架必须通过遍历整个DOM树来寻找对象。

为什么获取一些属性或调用方法也会导致回流?

因为以上操作都需要返回最新的布局信息,因此浏览器不得不触发回流重绘来返回正确的值。

3. 可用的优化策略

针对重排(Reflow)和重排(Reflow),解决问题的关键是:减少因DOM操作,引起的重排(Reflow)。

3.1 在DOM外,执行尽量多的变更操作

// Bad
for (var i=0; i < items.length; i++){
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("Option " + i);
    list.appendChild(item);
}

// Good
// 使用容器存放临时变更, 最后再一次性更新DOM
var fragment = document.createDocumentFragment();
for (var i=0; i < items.length; i++){
    var item = document.createElement("li");
    item.appendChild(document.createTextNode("Option " + i);
    fragment.appendChild(item);
}
list.appendChild(fragment);

3.2 操作DOM前,先把DOM节点删除或隐藏

使用display: none将元素从渲染树中完全移除,元素既不可见,也不是布局的组成部分,之后在该DOM上的操作不会触发回流与重绘,操作完之后再将display属性改为显示,只会触发这一次回流与重绘。

提醒:visibility : hidden的元素只对重绘有影响,不影响重排。

// Good
list.style.display = "none";  
for (var i=0; i < items.length; i++){  
    var item = document.createElement("li");  
    item.appendChild(document.createTextNode("Option " + i);  
    list.appendChild(item);  
}  
list.style.display = "";

3.3 减少样式属性修改

// Bad
const el = document.querySelector('.box')
el.style.margin = '5px'
el.style.borderRadius = '12px'
el.style.boxShadow = '1px 3px 4px #ccc'

// Good
.update{
  margin: 5px;
  border-dadius: 12px;
  box-shadow: 1px 3px 4px #ccc
}
const el = document.querySelector('.box')
el.classList.add('update')

3.4 缓存临时DOM

如果需要对DOM进行多次访问,尽量使用局部变量缓存该DOM

避免使用table布局,可能很⼩的⼀个⼩改动会造成整个table的重新布局。

// Bad
document.getElementById("myDiv").style.left = document.getElementById("myDiv").offsetLeft +  
document.getElementById("myDiv").offsetWidth + "px";

// Good
var myDiv = document.getElementById("myDiv");  
myDiv.style.left = myDiv.offsetLeft + myDiv.offsetWidth + "px";  

3.5 documentFragment

使用documentFragment创建一个DOM文档片段,在它上面批量操作DOM,操作完成之后,再添加到文档中,这样只会触发一次重排。

const el = document.querySelector('.box')
const fruits = ['front', 'nanjiu', 'study', 'code'];
const fragment = document.createDocumentFragment();
fruits.forEach(item => {
  const li = document.createElement('li');
  li.innerHTML = item;
  fragment.appendChild(li);
});
el.appendChild(fragment);

3.6 克隆节点,修改完再替换原始节点

const el = document.querySelector('.box')
const fruits = ['front', 'nanjiu', 'study', 'code'];
const cloneEl = el.cloneNode(true)
fruits.forEach(item => {
  const li = document.createElement('li');
  li.innerHTML = item;
  cloneEl.appendChild(li);
});
el.parentElement.replaceChild(cloneEl,el)

3.7 DOM脱离普通文档流

使用CSS定位absolutefixed让元素脱离普通文档流,使用绝对定位会使的该元素单独成为渲染树中 body的一个子元素,重排开销比较小,不会对其它节点造成太多影响。

3.8 浏览器的优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才会进行批量修改并清空队列。但是,在获取布局信息的时候,会强制刷新队列,比如当你访问以下属性或者使用以下方法:

  • clientTop、clientLeft、clientWidth、clientHeight
  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • getComputedStyle()
  • getBoundingClientRect

以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。

总结

本文简单介绍了浏览器为什么会选择DOM作为文本解析。阐述了造成DOM性能问题的原因即重绘和重排,同时总结了可用的几个DOM性能优化策略。