Reflow、Repaint、Composite 到底有什么区别?
做前端的人多多少少都听过这句忠告:”少触发 reflow,能用 transform 就别用 top/left”。但如果追问一句”reflow 和 repaint 具体差在哪儿?composite 又是什么阶段?”,很多人其实说不清楚。这篇文章就把这三者放在浏览器渲染管线里,一次讲透。
先搞清楚:浏览器是怎么把一个页面画出来的
要理解 reflow、repaint、composite 的区别,得先知道浏览器渲染一帧画面大致要走哪几步。简化后的流程是这样的:
- Style(样式计算):将 CSS 规则应用到 DOM 节点上,算出每个元素最终生效的样式。
- Layout(布局,也就是 Reflow):根据样式和 DOM 结构,计算每个元素在页面中的几何信息——位置、大小、盒模型。
- Paint(绘制,也就是 Repaint):把每个元素的可视外观(颜色、边框、阴影、文字等)转换成一系列绘制指令,光栅化成实际的像素。
- Composite(合成):将绘制好的多个图层,按照正确的层叠顺序、位置和变换关系,合并成最终显示在屏幕上的一帧画面。
这四步是有依赖关系的:Style 变化可能引发 Layout,Layout 变化必然引发 Paint(因为几何形状变了,画面肯定要重新画),Paint 之后一定要经过 Composite 才能显示出来。但反过来,并不是每次改动都要从头跑完整个流程——这正是优化的关键所在。
Reflow:几何信息变了,重新计算布局
Reflow(有的浏览器文档里也叫 Layout)指的是浏览器重新计算元素几何属性的过程:宽高、位置、边距等等。
什么情况会触发 Reflow:
- 改变元素的尺寸(
width、height、padding、border等) - 改变元素的位置(
top、left,或者改变position属性本身) - 增删 DOM 节点,或者改变文本内容导致排版变化
- 改变字体大小、字体族
- 调整浏览器窗口大小
- 甚至只是读取某些属性(如
offsetWidth、scrollTop、getBoundingClientRect()),也可能强制浏览器提前完成一次布局计算,业内通常叫”强制同步布局”或者”布局抖动”(layout thrashing)
Reflow 之所以是三者中开销最大的一个,原因在于它的连锁反应:一个元素的尺寸变化,很可能影响它的兄弟节点、父节点,甚至波及整个文档的布局。比如一个 flex 容器里某个子项的宽度变了,整行甚至整个容器都可能要重新排布。这就是为什么频繁读写布局相关属性、在循环里反复触发 reflow,会让页面明显卡顿。
Repaint:外观变了,但骨架没变
Repaint 指的是元素的几何信息没有变化,但外观发生了变化,需要重新绘制像素。
什么情况会触发 Repaint(但不触发 Reflow):
- 改变颜色,比如
color、background-color - 改变
visibility(注意:display: none会触发 reflow,因为元素直接从文档流里消失了;而visibility: hidden只是让元素不可见,不影响布局,所以只触发 repaint) - 改变
box-shadow、outline、border-radius等纯视觉效果属性
Repaint 的开销比 Reflow 小,因为它不需要重新计算任何几何关系,只需要按照已有的布局信息重新画一遍像素。但如果重绘的区域很大(比如整个可视区域的背景色变化),成本依然不容小觑。
需要特别说明的是:Reflow 之后必然伴随 Repaint,因为元素的形状、位置变了,之前画好的像素自然作废,必须重画。所以严格来说,Reflow 是 Repaint 的”超集触发条件”,但反过来不成立——Repaint 完全可以独立发生,不需要经过 Reflow。
Composite:把画好的图层拼在一起显示出来
Composite 是很多前端开发者最陌生的一环,但它恰恰是性能优化里最值得利用的部分。
现代浏览器在渲染时,会把页面拆分成若干个图层(layer)。哪些元素会被单独提升为一个图层?常见的情况包括:使用了 transform、opacity 做动画的元素,will-change 声明的元素,<video>、<canvas> 元素,设置了 position: fixed 的元素等等,浏览器出于性能考虑会把它们放到独立的合成层里。
每个图层会各自独立完成 Paint,然后交给 GPU,由合成线程(compositor thread)负责把这些图层按照层叠顺序、位置、透明度、变换矩阵组合成最终画面。这一步的关键优势在于:如果只是图层的位置或透明度发生变化,浏览器完全不需要重新走 Layout 和 Paint,只需要在 GPU 上重新合成这几个图层就行。
这就是为什么”用 transform: translate() 代替改 top/left 做动画”、”用 opacity 做淡入淡出而不是改 visibility 或者背景色透明度”会被反复强调——它们能让动画完全跳过 Layout 和 Paint 阶段,直接在 Composite 阶段完成,而 Composite 阶段是可以交给 GPU 并行处理的,帧率自然更流畅。
三者开销对比与优化思路
把三个阶段串起来看,性能开销大致是这样一个递减关系:
Reflow(涉及 Layout + Paint + Composite) > Repaint(涉及 Paint + Composite) > 仅 Composite
据此,前端性能优化中一条很实用的经验法则是:尽量把变化控制在”只触发 Composite”的层面。具体做法包括:
- 做动画效果时,优先使用
transform(平移、缩放、旋转)和opacity,避免使用top、left、width、height这类会触发 Layout 的属性 - 对于会频繁执行动画的元素,可以通过
will-change: transform提前告诉浏览器”这个元素接下来要变化”,让浏览器提前把它提升为独立合成层(但不要滥用,图层太多反而会增加内存开销和层间合成的成本) - 避免在循环中交替读写布局属性(比如先设置
style.width,接着又读offsetWidth),这种写法会强制浏览器插入额外的同步布局计算,业内称为”布局抖动”,是很典型的性能反模式 - 批量修改 DOM 时,可以先把元素从文档流中”拿出来”(比如设置
display: none,或者用DocumentFragment离线操作),改完再放回去,避免每次修改都触发一次全量重排
小结
用一句话总结这三者的关系:Reflow 关心”元素在哪、多大”,Repaint 关心”元素长什么样”,Composite 关心”这些画好的图层怎么拼在一起显示出来”。它们之间是层层递进又并非完全绑定的关系——改布局必然导致重绘和合成,改外观只需要重绘和合成,而如果变化恰好只发生在独立合成层上,浏览器甚至可以跳过布局和绘制,直接在 GPU 上完成合成。
理解这条渲染管线,不是为了背诵”哪些属性触发 reflow”这张表,而是为了在写动画、做交互的时候,能下意识地问自己一句:这个改动,究竟会让浏览器重新算一遍布局,还是只需要在 GPU 上挪一下图层?想清楚这个问题,很多性能问题在写代码的当下就能被避免,而不是等上线后再去 Performance 面板里排查。