前言
先来回顾一下浏览器 渲染流程 的大概过程:构建 DOM 树(DOM)、样式计算(Style)、获取布局树(Layout)、生成图层树(Layer)、图层绘制(Paint)、栅格化处理(Raster)、合成显示(DrawQuad)。
理解了浏览器的渲染流程之后,再去理解重排、重绘和合成的概念就很容易了。
重排
重排,也叫回流,是更新了元素的几何属性之后,浏览重新触发布局(Layout),更新完整的渲染流程的过程。
它的关键 在于修改了元素的几何属性,例如改变元素的宽度、高度等,这个修改会让浏览器从布局开始更新整个渲染流水线,这个开销是最大的。如下图所示:
一些会引起重排操作的情况:
- 添加或者删除可见的 DOM 元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
根据改变的范围和程度,渲染树中或大或小的部分需要重新计算,有些改变会触发整个页面的重排,比如,滚动条出现的时候或者修改了根节点。
重绘
重绘,因为没有引起几何位置的变化,所以浏览器会直接进入图层绘制阶段(Paint),然后执行之后的渲染流程。
它的关键 在于更新的是绘制属性,不会引起几何位置的变化,例如修改了元素的背景颜色或者文字颜色,它会从图层绘制阶段开始更新渲染流水线,相较于重排操作,重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。
所以说,重排必定会发生重绘,重绘不一定会引发重排。
合成
从渲染流水线的角度看,修改布局属性会引起重排,修改绘制属性会引起重绘,那如果修改一个既不要布局也不要绘制的属性,渲染流水线又会怎么处理呢?
如果修改一个既不要布局也不要绘制的属性,渲染引擎将直接跳过布局和图层绘制的阶段,只执行后续的合成操作,这个过程叫做 合成。
比如使用了 CSS 的 transform
来实现动画效果,这可以避开重排和重绘阶段,直接在非主线程上执行合成动画操作。这样的效率是很高的,因为是在非主线程上合成,并没有占用主线程的资源,另外也避开了布局和绘制两个子阶段,所以 相对于重绘和重排,合成能大大提升绘制效率。
浏览器的优化
现代的浏览器都是很聪明的,由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会 通过队列机制来批量更新布局。浏览器会将修改操作放入到队列里,至少一个浏览器刷新(即16.6ms
)才会清空队列。这样可以让多次的重排、重绘变成一次回流重绘。
但是,当在 获取布局信息 的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发重排与重绘来确保返回正确的值。
主要包括以下属性或方法:
offsetTop
、offsetLeft
、offsetWidth
、offsetHeight
scrollTop
、scrollLeft
、scrollWidth
、scrollHeight
clientTop
、clientLeft
、clientWidth
、clientHeight
width
、height
getComputedStyle()
getBoundingClientRect()
以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会强制刷新渲染队列。如果要使用它们,最好将值缓存起来。
如何减少重排和重绘?
首先要知道为什么减少重排重绘能优化 Web 性能?
在渲染流水线的视角下,减少重排重绘,省去了布局和绘制阶段,这样少了渲染进程的主线程和非主线程的很多计算和操作,是能够加快页面的渲染展示的,而我们的目的正在于此。
如何减少重排和重绘?
浏览器本身会做一些优化,但是我们平时写代码也要注意避免重排和重绘:
在写 JS 代码时:
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性。 - 避免频繁操作DOM,创建一个
documentFragment
,在它上面应用所有 DOM 操作,最后再把它添加到文档中;或者在修改时隐藏元素,修改完成之后再显示。 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
- 对具有复杂动画的元素使用 绝对定位,使它 脱离文档流,否则会引起父元素及后续元素频繁回流。
在写 CSS 代码时可以:
- 使用
tranform
替代top
,tranform
是合成属性,不会引起重排和重绘 - 使用
visibility
替换display:none
,前者只会重绘,后者改变布局会引发重排 - 避免使用
table
布局,可能很小的一个小改动会造成整个table
的重新布局,即重排 - 尽可能在 DOM 树的最末端改变
class
,重排是不可避免的,但可以减少其影响。尽可能在DOM树的最末端改变class
,可以限制了重排的范围,使其影响尽可能少的节点 - 避免设置多层内联样式,CSS 选择符从右往左匹配查找,避免节点层级过多
- 将动画效果应用到
position
属性为absolute
或fixed
的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是重排,同时,控制动画速度可以选择requestAnimationFrame
- 避免使用 CSS 表达式,可能会引发重排
- 将频繁重绘或者重排的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。如
will-change
、video
、iframe
等标签,浏览器会自动将该节点变为图层。 - CSS3 硬件加速(GPU加速),使用 CSS3 硬件加速,可以让
transform
、opacity
、filters
这些动画不会引起重排重绘。但是对于动画的其它属性,比如background-color
这些,还是会引起重排重绘的,不过它还是可以提升这些动画的性能。
总结
- 重排 是更新了 元素的几何属性 之后,浏览重新触发 布局(Layout),更新完整的渲染流程的过程
- 重绘 是修改了 元素的绘制属性 之后,浏览器会直接进入 图层绘制阶段(Paint),然后执行之后的渲染流程的过程
- 重排必定会发生重绘,重绘不一定会引发重排
- 合成 是修改一个既不要布局也不要绘制的属性之后,渲染引擎直接跳过布局和图层绘制的阶段,只执行后续的 合成操作 的过程。
- 减少重排和重绘可以优化 Web 性能,浏览器通过队列机制来优化,平时写代码也要注意避免重排和重绘。