减少回流重绘的方法及相关的性能测试结果分析

回流必将引起重绘,而重绘不一定引起回流
我们经常会看到上面这句话,那么回流和重绘是什么呢

回流

计算DOM节点在设备视口(viewport)内的确切位置和大小的过程,会在页面节点的几何属性或者布局发生变化时发生的

发生回流的情况

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

重绘

重绘的发生,是由于节点的几何属性发生改变或者由于样式发生改变但又不会影响布局的改变引起的,通过可见节点和具体样式信息,将渲染树的每个节点都转换为屏幕上的实际像素

发生重绘的情况

发生回流的时候,也会发生重绘,所以上面的情况同样会触发重绘
我的理解是,只要页面内容发生变化,都会导致重绘的发生(如果不对请指正)

减少回流重绘方法和性能测试

tip:在下面的性能测试中,我给出的图是某一次测试的,但实际上每种方法我都测试了很多次,每次都达到时间消耗变少的效果

1. 对那些DOM元素的操作,不要逐一操作,而是将其一次性操作,直接使用class或者一次性写入多个style

测试demo

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.newClass{
margin:10px;
font-size: 20px;
color:#f00;
}
</style>
</head>
<body>
<button onclick="oneByone()">一个个修改</button>
<button onclick="byClass()">使用class修改</button>
<li>111</li>
<!-- 下面还有999个li标签 -->
<!-- ... -->
<script>
let li = document.querySelectorAll("li")

function oneByone() {
for (let i = 0; i < li.length; i++) {
li[i].style.margin = "10px";
li[i].style.fontSize = "20px";
li[i].style.color = "#f00"
}
}

function byClass() {
for (let i = 0; i < li.length; i++) {
li[i].className = "newClass"
}
}
</script>
</body>
</html>

在这里,我通过点击两个按钮,使用performance监控,将监控图进行比较,这里主要会看的是Scripting和Rendering,即js执行和css解析,如图


可以看到,通过class来修改样式的时候,Scripting和Rendering都比一个一个修改消耗的时间少

2. 对于一些样式的修改,尽量采用不会影响页面布局的修改

比如display:none和visibility: hidden,在可能的情况下尽量使用visibility
测试demo

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button onclick="vi()">vBtn</button>
<button onclick="di()">dBtn</button>
<li></li>
<!-- 下面还有999个li标签 -->
<!-- ... -->
<script>
let li = document.querySelectorAll("li")

function di() {
for (let i = 0; i < li.length; i++) {
li[i].style.display = "none"
}
}

function vi() {
for (let i = 0; i < li.length; i++) {
li[i].style.visibility = "hidden"
}
}
</script>
</body>
</html>

这里我同样使用performance来监控性能,按照我们上面的理论,使用display:none会引起回流,而使用visibility:hidden不会,所以我们重点看火焰图中的Rendering也就是紫色的部分

显然可以看出,按下vBtn时消耗的时间是43ms,而按下dBtn消耗的时间是153ms,区别是很明显的

3. 对那些需要多次修改的操作,可以先使DOM元素脱离文档流,然后对其进行多次修改,最后再将其放回到文档流中,这样只会引起两次回流

测试demo

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button onclick="inFloat()">直接修改</button>
<button onclick="outOfFloat()">移出文档流</button>
<li>111</li>
<!-- 下面还有999个li标签 -->
<!-- ... -->
<script>
let li = document.querySelectorAll("li")

function inFloat() {
for (let i = 0; i < li.length; i++) {
li[i].style.margin = "10px";
li[i].style.padding = "10px";
li[i].style.border = "1px solid";
li[i].style.fontSize = "20px";
}
}

function outOfFloat() {
for (let i = 0; i < li.length; i++) {
li[i].style.display = "none";
li[i].style.margin = "10px";
li[i].style.padding = "10px";
li[i].style.border = "1px solid";
li[i].style.fontSize = "20px";
li[i].style.display = "list-item";
}
}
</script>
</body>
</html>


如图,我们可以看到,脱离文档流后,Rendering的时间变少了,符合我们上面的理论,但是,可以看到Scripting那里,脱离文档流的时候,js执行的时间变长了,当然,这是因为我们为了让元素脱离文档流执行的操作导致的,我们可以进一步查看scripting的内容



如图可以直观地看到,outOfFloat方法的消耗比inFloat方法的消耗多,所以采取这种方法来减少回流和重绘性能消耗的时候,要考虑到回流和重绘减少的时间,是否会多于增加的js语句执行带来的消耗,这样才能真正地做到整体的性能优化

4. 将会导致回流重绘的修改放在文档流比较后的位置

我们修改页面的元素后,如果文档流前面部分的布局修改了,可能会导致文档流后面的内容也要进行回流重绘,而如果这部分修改是放在后面的,那回流和重绘的消耗就会比较小了

测试demo

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<button onclick="front()">修改前面一半</button>
<button onclick="behind()">修改后面一半</button>
<li>111</li>
<!-- 下面还有999个li标签 -->
<!-- ... -->
<script>
let li = document.querySelectorAll("li")

function front() {
for (let i = 0; i < li.length / 2; i++) {
li[i].style.margin = "10px";
li[i].style.padding = "10px";
li[i].style.border = "1px solid";
li[i].style.fontSize = "20px";
}
}

function behind() {
for (let i = li.length / 2; i < li.length; i++) {
li[i].style.margin = "10px";
li[i].style.padding = "10px";
li[i].style.border = "1px solid";
li[i].style.fontSize = "20px";
}
}
</script>
</body>
</html>

结果如图所示,将会导致回流和重绘的元素放在文档流后面,在css解析部分还是会消耗比较少的时间,虽然这里时间相差不多,但我多次测试时每次都是改变后面消耗会更少