回流和重绘是什么?

在HTML中,每一个元素都可以理解为一个盒子,在浏览器解析过程中,会涉及到回流和重绘:

  • 回流:布局引擎会根据各种样式计算每一个盒子在页面上的大小与位置。回流是布局或者几何属性需要改变就称为回流。

一个元素的回流可能会导致了其所有子元素以及DOM中紧随其后的节点、祖先节点元素的随后的回流。

  • 重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制。 由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的称做重绘。

浏览器的解析渲染机制

浏览器采用流式布局模型

  • 解析HTML,生成DOM树,解析CSS,生成CSSOM树。
  • 将DOM和CSSOM树结合,生成渲染树(Render TREE)。
  • Layout(回流): 根据生成的渲染树,进行回流(layout),得到节点的几何信息(位置,大小)。
  • Painting(重绘): 根据渲染树以及回流得到的几何信息,得到节点的绝对像素。
  • Display: 将像素发给GPU,展示在页面上。

当页面初始渲染阶段,回流重绘都会被触发
当修改DOM的几何属性(宽高或显示隐藏元素等影响页面布局的操作),浏览器需要触发回流操作重新计算节点的几何属性,进行重绘。
当修改DOM的样式信息,则仅仅触发重绘。

如何触发回流和重绘

回流触发时机

  • 添加或删除可见的DOM元素
  • 元素的位置发生变化
  • 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
  • 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
  • 页面一开始渲染的时候(这避免不了)
  • 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
  • 还有一些容易被忽略的操作:获取一些特定属性的值,这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流:
1
2
3
4
5
6
offsetTop、offsetLeft、offsetWidth、offsetHeight
scrollTop、scrollLeft、scrollWidth、scrollHeight
clientTop、clientLeft、clientWidth、clientHeight
width、height
getComputedStyle()
getBoundingClientRect()

重绘触发时机

  • 触发回流一定会触发重绘
  • 颜色的修改
  • 文本方向的修改
  • 阴影的修改

浏览器优化机制

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,至少一个浏览器刷新(即16.6ms)或者操作达到了一个阈值,才清空队列.

当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,包括前面讲到的offsetTop等方法都会返回最新的数据,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值。

如何减少回流和重绘

CSS

  • 如果想设定元素的样式,通过改变元素的class类名(尽可能在DOM树的最里层), 避免设置多项内联样式
    当通过修改节点的样式属性时,浏览器需要重新计算样式和更新渲染树,然后进行回流和重绘。这是因为在修改样式属性后,浏览器需要先确定渲染树中所有相关节点的样式变化,然后更新它们的位置和大小,并重新计算页面布局,这就导致了浏览器的性能开销。与此相比,修改 class 类名只需要对该类名所匹配的节点进行更新,而不需要计算整个页面的布局和渲染树,这可以大大减少浏览器的回流和重绘,提升性能表现。

  • 应用元素的动画,使用 position 属性的 fixed 值或 absolute 值使元素脱离文档流,从而减少对其他元素的影响,这样只是一个重绘,而不是回流.

  • 避免使用 table 布局,table 中每个元素的大小以及内容的改动,都会导致整个 table 的重新计算

由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,但table及其内部元素除外,他们可能需要多次计算,通常要花3倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一。

  • 使用css3硬件加速(GPU加速),可以让transform、opacity、filters这些动画不会引起回流重绘.但是对于动画的其它属性,比如background-color这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
  • 控制动画速度可以选择 requestAnimationFrame,详见探讨 requestAnimationFrame。
  • 避免使用 CSS 的 JavaScript 表达式
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多。
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-change、video、iframe等标签,浏览器会自动将该节点变为图层。
    JavaScript
  • 在使用 JavaScript 动态插入多个节点时, 可以使用DocumentFragment. 创建后一次插入. 就能避免多次的渲染性能。
  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <div>
    <a> <span></span> </a>
    </div>
    <style>
    span {
    color: red;
    }
    div > a > span {
    color: red;
    }
    </style>

对于第一种设置样式的方式来说,浏览器只需要找到页面中所有的 span 标签然后设置颜色,但是对于第二种设置样式的方式来说,浏览器首先需要找到所有的 span 标签,然后找到 span 标签上的 a 标签,最后再去找到 div 标签,然后给符合这种条件的 span 标签设置颜色,这样的递归过程就很复杂。所以我们应该尽可能的避免写过于具体的 CSS 选择器,然后对于 HTML 来说也尽量少的添加无意义标签,保证层级扁平。

回流示例

在下面的HTML片段中,对该段落<p>标签回流将会引发强烈的回流,因为它是一个子节点。这也导致了祖先的回流(div.error和body – 视浏览器而定)。此外,<h5><ol>也会有简单的回流,因为其在DOM中在回流元素之后。大部分的回流将导致页面的重新渲染。

回流必定会发生重绘,重绘不一定会引发回流。

1
2
3
4
5
6
7
8
9
10
11
12

<body>
<div class="error">
<h4>我的组件</h4>
<p><strong>错误:</strong>错误的描述…</p>
<h5>错误纠正</h5>
<ol>
<li>第一步</li>
<li>第二步</li>
</ol>
</div>
</body>


总结:大功告成✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️✌️