关于回流和重绘

一、浏览器渲染过程

讲回流和重绘之前,我们先来了解下浏览器的渲染过程,如下图:

浏览器渲染过程

从图中看到,浏览器的渲染过程分以下几步进行:
1、将HTML解析为DOM树,将CSS解析为CSSOM树
2、将DOM树和CSSOM树进行计算合成,生成渲染树Render Tree
3、Layout(回流):根据渲染树Render Tree进行回流Layout,得到节点的几何信息(位置、大小)
4、Painting(重绘):根据渲染树Render Tree,以及回流(Layout)得到的节点几何信息,得到节点的绝对像素
5、Display:将各个节点绘制在屏幕上

其中,浏览器构建渲染树Render Tree过程,如下图:

render-tree

1、从 DOM 树的根节点开始遍历每个可见节点。

  • 某些节点不可见(例如脚本标记、元标记等),因为它们不会体现在渲染输出中,所以会被忽略。
  • 某些节点通过 CSS 隐藏,因此在渲染树中也会被忽略,例如,上例中的 span 节点不会出现在渲染树中,因为有一个显式规则在该节点上设置了“display: none”属性。备注:利用visibility和opacity隐藏的节点,还是会显示在渲染树上的

2、对于每个可见节点,为其找到适配的 CSSOM 规则并应用它们。

3、根据每个可见节点的内容和样式,生成渲染树Render Tree

二、回流重绘的概念

1、回流

概念:回流就是浏览器根据Render Tree在视口(viewport)计算节点具体位置和大小的过程。
为弄清每个对象在网页上的确切大小和位置,浏览器从渲染树的根节点开始进行遍历。让我们考虑下面这样一个简单的实例:

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Critial Path: Hello world!</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world!</div>
</div>
</body>
</html>

以上代码包含两个嵌套div元素:父元素div将宽度设置为视口宽度的 50%,子元素div将其宽度设置为其父元素的50%,即视口宽度的25%。而在回流这个阶段,我们就需要根据视口具体的宽度,将其转为实际的像素值。如下图:
layout-viewport

2、重绘

概念:根据渲染树和回流阶段,我们知道了可见节点的几何信息(位置、大小)和样式,那么我们就可以将渲染树的每个节点转换为屏幕上的实际像素,这个阶段就叫做重绘。

3、何时发生回流重绘

从以上我们知道了,回流阶段主要是计算节点的几何信息(位置、大小),那么当页面的布局和几何信息发生变化时,就会发生回流。比如:

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
  • 页面一开始渲染的时候(这肯定避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)

总结一句话就是:回流必定会引起重绘,重绘不一定会引起回流。

三、浏览器优化机制

为了提高性能,浏览器通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制队列刷新,比如当你使用了如下属性或方法:

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

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

四、如何避免、较少回流重绘

那么,我们如何避免或者减少回流重绘呢?

1、CSS

  • 使用css的calss修改样式,尽可能在DOM树的最末端改变class
  • 使用 visibility 替换 display: none,因为前者只会引起重绘,后者会引发回流
  • 避免使用table布局
  • 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
  • 将动画效果应用到position属性为absolute或fixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流,同时,控制动画速度可以选择 requestAnimationFrame
  • CSS3 硬件加速(GPU加速),使用css3硬件加速,可以让transform、opacity、filters这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。

2、JavaScript

  • 动态改变样式使用cssText

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 不推荐写法
    const el = document.getElementById('box');
    el.style.padding = '4px';
    el.style.borderLeft = '2px';
    el.style.width = '50px';
    // 推荐写法:
    const el = document.getElementById('box');
    el.style.cssText += 'padding: 4px; border-left: 2px; width: 50px;';
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。

  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。

参考文献