DOM 的回流(reflow)重绘(repaint)。

1. 浏览器页面渲染流程

  1. 浏览器把获取到的HTML代码解析成1个DOM树。DOM树里包含了所有HTML标签,包括 display:none 隐藏,还有用JS动态添加的元素等。

  2. 浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式

  3. DOM Tree 和样式结构体组合后构建render tree, render tree类似于DOM tree,但区别很大,render tree能识别样式,render tree中每个NODE都有自己的style,而且 render tree不包含隐藏的节点 (比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。注意 visibility:hidden隐藏的元素还是会包含到 render tree中的,因为visibility:hidden 会影响布局(layout),会占有空间。根据CSS2的标准,render tree中的每个节点都称为Box (Box dimensions),理解页面元素为一个具有填充、边距、边框和位置的盒子。

  4. 一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了。

DOM 的回流和重绘 - 图1

2. 重绘

当某一个 DOM 元素样式更改(只影响元素的外观、风格等),浏览器会重新渲染这个元素,这就是重绘。

  1. box.style.color = "red";
  2. //...中间还有一些代码
  3. box.style.fontSize = '16px';
  4. //=> 修改紧挨的话,也只是触发一次重绘。

上面操作触发了两次重绘,性能上有所消耗,真实项目中为了优化这个性能,我们最好一次性把需要修改的样式搞定。

  1. //=> 修改放在一起
  2. box.style.color = "red";
  3. box.style.fontSize = '16px';
  4. //... 执行其他代码
  5. //=> 直接使用类来修改样式
  6. .xxx {
  7. color: red;
  8. fontSize: '16px';
  9. }
  10. box.className = 'xxx';

重绘会消耗性能,但是不会消耗太大。相当于从浏览器渲染的第三步开始。

3. 回流

当 DOM 元素的结构或者位置发生改变都会引发回流。

  • 添加或删除可见的 DOM 元素

  • 元素位置的改变

  • 元素尺寸的改变(边距、填充、边框、宽度、高度)

  • 内容改变(文本改变或者图片大小改变而引起的计算值宽度和高度改变)

  • 页面渲染初始化

  • 浏览器窗口尺寸改变(resize 事件发生时)

回流特别消耗性能,需要从第一步开始重新渲染。

4. 优化

4.1 浏览器自身的优化策略

  • 浏览器会维护一个队列,把所有会引起回流、重绘的操作放入这个队列。

  • 等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会释放队列,进行一个批处理。

这样就会让多次的回流、重绘变成一次回流重绘。

但有时候我们写的一些代码可能会强制浏览器提前释放队列,这样浏览器的优化可能就起不到作用了。比如:当你向浏览器请求一些样式信息的时候,就会让浏览器释放队列,才能获取的最新的样式。

4.2 基于文档碎片减少回流

在虚拟空间中开辟的一个容器:

  • 每当创建一个元素,先把它存放到文档碎片中(千万不要放到页面中,避免回流)

  • 当我们把需要的元素都创建完成,并且都添加到文档碎片中,再统一把文档碎片放到页面中(只会引发一次回流)

  1. //=> 创建文档碎片
  2. let frg = document.createDocumentFragment();
  3. data.forEach((item)=> {
  4. let List = document.createElement('li');
  5. //=> 每次把元素放入文档碎片中
  6. frg.appendChild(li)
  7. });
  8. //=> 最后再统一放入父元素中
  9. parent.appendChild(frg);
  10. //=> 手动销毁
  11. frg = null;

4.3 基于字符串拼接减少回流

把原有容器中的结构都以字符串的方式获取到,然后拼接成新的字符串,最后统一在插入到原有容器中。

  1. let str = ``;
  2. data.forEach(function (item) {
  3. str += `<li data-time="${time}" data-hot="${hot}" data-price="${price}">
  4. <a href="#">
  5. <img src="${picImg}" alt="">
  6. <p title="${title}">${title}</p>
  7. <span>¥${price}</span>
  8. <span>${hot}</span>
  9. <span>${time}</span>
  10. </a>
  11. </li>`;
  12. });
  13. parent.innerHTML = str;

理论上说,这种方式性能不如前面提到的文档碎片。因为这里还要把字符串变成 HTML 元素。但是这种方式很简单,所以真实项目中较常用。

4.4 分离读写

把对 DOM 的读和写分开放到一起。减少在写操作的过程中又进行读操作,是基于前面的浏览器自身优化策略实现的。一次性把样式都操作完成。

// 引发两次回流
box.style.top = '100px';
box.style.top;
box.style.left = '100px';

// 引发一次回流
box.style.top = '100px';
box.style.left = '100px';
box.style.top;