代码视界

Hanpeng Chen的个人博客

面试经常提到的重排和重绘,你真的了解吗?

本文于 1120 天之前发表,文中内容可能已经过时。

上一篇文章 「浏览器渲染流程」你知道HTML、CSS、JS文件在浏览器中是如何转化成页面的吗? 学习了浏览器的页面渲染流程,在文章的最后我们提到了两个和渲染流程有关的概念:重排和重绘。理解这两个概念对我们做Web性能优化会有很大的帮助。

重排(reflow)

概念

当更新了元素的几何属性,那么浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排,也称为“回流”。

例如我们通过JS或CSS修改了元素的宽度和高度,那么浏览器会触发重新布局,解析之后的一系列子阶段,这个过程就叫重排。

渲染树的节点发生改变,影响了该节点的几何属性,导致该节点位置发生变化,此时就会触发浏览器重排并重新生成渲染树。重排意味着节点的几何属性改变,需重新计算并生成渲染树,导致渲染树的全部或部分发生变化。

重排需要更新完整的渲染流水线,所以开销也是最大的。

常见的引起重排属性和方法

任何会改变元素的位置和尺寸大小的操作,都会触发重排。常见的例子如下:

  • 添加或删除可见的DOM元素
  • 元素尺寸改变
  • 内容变化,比如在input框中输入文字
  • 浏览器窗口尺寸改变
  • 计算offsetTop、offsetLeft等布局信息
  • 设置style属性的值
  • 激活CSS伪类,例如 :hover
  • 查询某些属性或调用某些方法

几何属性

几何属性:包括布局、尺寸等可用数学几何衡量的属性。

  • 布局:display、float、position、list、table、flex、columns、grid
  • 尺寸:margin、padding、border、width、height

获取布局信息的属性或方法

获取布局信息的属性如下:

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

看到这里有的人可能会疑惑,我们只是获取这些属性值,并没有改变它,为什么会触发重排?

现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列。但是!当你获取布局信息的操作的时候,会强制清空队列,因为队列中,可能会有影响到这些值的操作,为了给我们最精确的值,浏览器会立即重排+重绘。

所以我们应避免频繁使用上述的属性。

重排的影响范围

浏览器渲染界面是基于流式布局模型的,所以触发重排时会对周围的DOM重新排列,影响的范围分两种:

全局范围

从根节点html开始对整个渲染树重新布局。

1
2
3
4
5
6
7
8
9
<body>
<div class="hello">
<p><strong>Name:</strong>BDing</p>
<h5>male</h5>
<ol>
<li>loving</li>
</ol>
</div>
</body>

上面代码中的p节点发生重排时,它的父节点div和body也会发生重排,甚至h5和ol节点也会受到影响。

局部范围

对渲染树的某部分或某一渲染对象进行重新布局。

例如:讲一个DOM元素的宽高等几何信息写死,然后在DOM元素内部触发重排,就只会重新渲染该DOM元素内部的元素,而不会影响到外界。

重绘(repaint)

概念

更新了元素的绘制属性,但没有改变布局,重新把元素外观绘制出来的过程叫做重绘。例如更改某些元素的背景颜色。

重绘并没有引起元素几何属性的改变,所以就直接进入绘制阶段,然后执行之后的一系列子阶段。

和重排相比,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。

重排一定会伴随重绘,重绘却不一定伴随重排。

外观属性

包括界面、文字等可用状态向量描述的属性

  • 界面:appearance、outline、background、mask、box-shadow、box-reflect、filter、opacity、clip、border-radius、background-size、visibility
  • 文字:text、font、word

性能优化

重排和重绘在操作节点样式时频繁出现,同时也存在很大程度上的性能问题。重排成本比重绘成本高得多,因为一个节点的重排可能导致子节点、兄弟节点或祖先节点的重排,所以我们要尽可能减少重排次数、重排范围。

使用visibility:hidden替换display:none

通过下面四个方面来看看两者有什么区别:

  • 占位表现

    • display:none:不占据空间
    • visibility:hidden:占据空间
  • 触发影响

    • display:none:触发重排重绘
    • visibility:hidden:触发重绘
  • 过渡影响

    • display:none:影响过渡不影响动画
    • visibility:hidden:过渡和动画都不影响
  • 株连效果

    • display:none:自身及其子节点全都不可见
    • visibility:hidden:自身及其子节点都不可见,但可声明子节点visibility:visible单独显示

避免使用Table布局

Table布局可能很小的一个改动就会造成整个table重排。

通常可用 ul、li、span等标签取代 table 系列标签生成表格

避免设置多层内联样式

浏览器的CSS解析器解析css文件时,对CSS规则是从右到左匹配查找,样式层级过多会影响重排重绘效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<style>
span {
color: red;
}

div > a > span {
color: red;
}
</style>

<div>
<a href="https://www.baidu.com">
<span>百度搜索</span>
</a>
</div>

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

将频繁重绘或重排的节点设置为图层

上一篇文章中我们构建完布局树之后,我们会进行分层,将页面分为很多个图层,如果不对图层添加关联,图层之间是不会相互影响的。

因此,在浏览器中将频繁重排或重绘的节点设置为一张新图层,那么新图层就能够阻止节点的渲染行为影响别的节点。

设置新图层的方法:

  • 将节点设置为video或iframe
  • 为节点添加 will-change 属性

使用requestAnimationFrame作为动画帧

动画速度越快,重排次数越多,浏览器刷新频率为60Hz,即每16.6ms更新一次,而requestAnimationFrame()正是以16.6ms的速度更新一次。所以可用requestAnimationFrame()代替setInterval()。

对于复杂动画效果,使用绝对定位让其脱离文档流

对于复杂动画效果,由于会经常的引起重排重绘,因此,我们可以使用绝对定位,让它脱离文档流。否则会引起父元素以及后续元素频繁的重排。

动态改变类而不改变样式

不要尝试每次操作DOM去改变节点样式,这样会频繁触发重排。

更好的方式是使用新的类名预定义节点样式,在执行逻辑操作时收集并确认最终更换的类名集合,在适合时机一次性动态替换原来的类名集合。

具体的实现可以看下HTML DOM元素属性 classList

避免触发同步布局事件

先来看下面的代码:

1
2
3
4
for (let i = 0; i <100; i++) {
const top = document.getElementById('list').style.top;
console.log(top)
}

上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。

上面代码中每次循环操作DOM都会发生重排,应该在循环外使用变量保存一些不会变化的DOM映射值。

1
2
3
4
const top = document.getElementById('list').style.top;
for (let i = 0; i <100; i++) {
console.log(top)
}

批量修改DOM

当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少重排重绘次数:

    1. 使元素脱离文档流
    1. 对其进行多次修改
    1. 将元素带回到文档中。

该过程的第一步和第三步可能会引起重排,但是经过第一步之后,对DOM的所有修改都不会引起重排,因为它已经不在渲染树了。

有三种方式可以让DOM脱离文档流:

  • 隐藏要操作的dom
    在要操作dom之前,通过display隐藏dom,当操作完成之后,再将dom的display属性置为可见,因为不可见的元素不会触发重排和重绘。

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

  • 复制节点,在副本上工作,然后替换它。

当然我们也可以使用框架来实现批量修改DOM,比如Vue、React。

CSS3硬件加速(GPU加速)

使用CSS3硬件加速,可以让 transformopacityfilters这些动画不会引起重拍重绘,但对于动画的其它属性,比如background-color这些,还是会引起重排重绘的,不过它还是可以提升这些动画的性能。

常见的触发CSS3硬件加速的CSS属性有:

  • transform
  • opacity
  • filters
  • will-change

启动硬件加速注意点:

  • 如果为太多元素使用CSS3硬件加速,会导致内存占用较大,也会从另一方面导致性能问题
  • 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。

我们通过下面这个例子来看验证一下CSS3硬件加速这个优化方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<body>
<style>
#container {
width: 300px;
height: 300px;
position: absolute;
border: 1px solid burlywood;
}

.rect {
width: 100px;
height: 100px;
left: 0;
top: 0;
background-color: gray;
}

.animate .rect {
animation: run-around 4s ease-in-out infinite;
}

@keyframes run-around {
0% {
transform: translate3d(0, 0, 0);
}

25% {
transform: translate3d(200px, 0, 0);
}

50% {
transform: translate3d(200px, 200px, 0);
}

75% {
transform: translate3d(0, 200px, 0);
}
}
</style>

<script>
function start() {
const el = document.getElementById("container")
el.classList.contains('animate') ? el.classList.remove('animate') : el.classList.add('animate')
}
</script>

<button onclick="start()">开启/停止动画</button>
<div id="container">
<div class="rect"></div>
</div>
</body>

通过Chrome浏览器的Performance捕获了一段时间的重排重绘情况,结果如下:

当动画进行的时候,没有发生任何的重排或重绘。

总结

通过上面的学习,我们可以总结出以下几点:

  • 重排是因为元素的几何属性更改触发的
  • 重绘是由于元素的绘制属性更改触发的
  • 触发重排也一定会触发重绘,触发重绘不一定会触发重排
  • 重排的成本高于重绘
  • 减少重排次数、重排范围是Web性能优化的基本思路

文章到这里正文内容就结束了,你是否已经清楚什么是重排和重绘?为什么减少重排重绘能够优化Web性能?欢迎留言讨论。

如果你觉得这篇内容对你有帮助的话:

  1. 点赞支持下吧,让更多的人也能看到这篇内容

  2. 关注公众号:前端极客技术,我们一起学习一起进步。

欢迎关注微信公众号: 『前端极客技术』『代码视界』
支付宝打赏 微信打赏

赞赏是不耍流氓的鼓励