浏览器的重绘与回流

在探讨浏览器的重绘与回流问题之前,让我们来梳理一下,当浏览器接收的服务器的响应,得到HTML之后,发生了些什么:

  • HTML被交给HTML解析器转化为一系列的Token
  • 根据Token构建Node,形成DOM Tree
  • 遇到CSS标签或者JS脚本标签就会启用新线程请求下载
  • CSS样式被CSS解析器解释成内部表示结构
  • CSS解析器工作完成后,将样式信息附合到DOM树上,形成Render Tree
  • 对Render Tree的各个节点计算布局信息,包括位置、尺寸等等
  • 根据Render Tree利用浏览器的UI渲染线程渲染页面

在此过程中,如果遇到script脚本时,HTML解析器将阻塞,等待脚本下载完成并执行,然后继续解析后面的文档,JS可能会修改文档的结构,比如document.write(),这就意味着加载后续的文档变得没有意义,这也是我们经常把JS脚本放在底部的原因,当然我们可以使用async与defer来异步或者延迟执行JS,请看下图:

async and defer

而对于CSS而言,它会阻塞Render Tree的构建,而不会阻塞DOM Tree的构建,同时它的解析和渲染将阻塞后续js的执行,因为js的执行可能会依赖于最新样式。

Render

浏览器重绘(repaint)

当一个元素外观被改变,但没有改变布局(宽高)的情况下发生重绘。例如改变visibility(元素是否可见,不可见也会占据空间,注意与display:none)的区别、outline(绘制于元素周围的一条线)、background-color等等。

浏览器回流(reflow)

有哪些行为会触发浏览器的回流呢?其实回流与我们平时时常提到的操作DOM元素慢是密切相关。

DOM元素的几何属性发生改变

DOM的变化影响了元素的几何属性(宽高),浏览器会重新计算元素的几何属性,会使Render Tree中受到影响的部分失效,从而产生新的Render Tree。当一个元素发送回流时,会使它的子节点,兄弟节点,甚至祖先节点的几何属性发生变化,触发整个页面的回流。

DOM元素的结构变化

节点的插入、删除、移动都会触发回流,如果在body元素前插入一个元素,会引发整个页面的回流,而在DOM树的叶子节点插入节点则不会对其它节点产生影响。

获取特定属性

例如: offset系列、scroll系列、client系列,获取这些值时应尽量缓存。

  • 调整窗口大小
  • 改变字体
  • 对于样式的修改
  • CSS伪类激活,例如:hover

触发回流的行为有很多,而回流也是不可避免的,我们只能尽可能最小化回流。

操作完整个节点后,再插入Render Tree中,多次回流合并为一次回流:

比如利用documentFragment:

1
2
3
4
5
6
7
var frag = document.createDocumentFragment();
for(var x = 0; x < 10; x++) {
var li = document.createElement("li");
li.innerHTML = "List item " + x;
frag.appendChild(li);
}
listNode.appendChild(frag);

对于动画元素,每一帧都会触发回流,使其脱离文档流,或使用requestAnimationFrame

position: absolute / fixed 或 float

对于requestAnimationFrame而言,它会将每一帧中的所有DOM操作集中在一起,在一次重绘或回流中完成。而对于隐藏或不可见的元素,则不会进行重绘与回流。

减少不必要的DOM深度,精简CSS,CSS避免复杂匹配与末尾通配符