作者:Hanpeng_Chen
公众号:前端极客技术
上一篇文章 「浏览器渲染流程」你知道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开始对整个渲染树重新布局。
<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规则是从右到左匹配查找,样式层级过多会影响重排重绘效率。
<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
。
避免触发同步布局事件
先来看下面的代码:
for (let i = 0; i <100; i++) {
const top = document.getElementById('list').style.top;
console.log(top)
}
复制代码
上文我们说过,当我们访问元素的一些属性的时候,会导致浏览器强制清空队列,进行强制同步布局。
上面代码中每次循环操作DOM都会发生重排,应该在循环外使用变量保存一些不会变化的DOM映射值。
const top = document.getElementById('list').style.top;
for (let i = 0; i <100; i++) {
console.log(top)
}
复制代码
批量修改DOM
当我们需要对DOM对一系列修改的时候,可以通过以下步骤减少重排重绘次数:
-
- 使元素脱离文档流
-
- 对其进行多次修改
-
- 将元素带回到文档中。
该过程的第一步和第三步可能会引起重排,但是经过第一步之后,对DOM的所有修改都不会引起重排,因为它已经不在渲染树了。
有三种方式可以让DOM脱离文档流:
- 隐藏要操作的dom
在要操作dom之前,通过display隐藏dom,当操作完成之后,再将dom的display属性置为可见,因为不可见的元素不会触发重排和重绘。
-
通过使用
DocumentFragment
创建一个dom碎片,在它上面批量操作dom,操作完成之后,再添加到文档中,这样只会触发一次重排。 -
复制节点,在副本上工作,然后替换它。
当然我们也可以使用框架来实现批量修改DOM,比如Vue、React。
CSS3硬件加速(GPU加速)
使用CSS3硬件加速,可以让 transform
、opacity
、filters
这些动画不会引起重拍重绘,但对于动画的其它属性,比如background-color这些,还是会引起重排重绘的,不过它还是可以提升这些动画的性能。
常见的触发CSS3硬件加速的CSS属性有:
- transform
- opacity
- filters
- will-change
启动硬件加速注意点:
- 如果为太多元素使用CSS3硬件加速,会导致内存占用较大,也会从另一方面导致性能问题
- 在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速,会产生字体模糊。
我们通过下面这个例子来看验证一下CSS3硬件加速这个优化方法:
<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性能?欢迎留言讨论。