Reflow、Repaint、Composite 到底有什么区别?

JavaScript1周前更新 admin
121 0

做前端的人多多少少都听过这句忠告:”少触发 reflow,能用 transform 就别用 top/left”。但如果追问一句”reflow 和 repaint 具体差在哪儿?composite 又是什么阶段?”,很多人其实说不清楚。这篇文章就把这三者放在浏览器渲染管线里,一次讲透。

先搞清楚:浏览器是怎么把一个页面画出来的

要理解 reflow、repaint、composite 的区别,得先知道浏览器渲染一帧画面大致要走哪几步。简化后的流程是这样的:

  1. Style(样式计算):将 CSS 规则应用到 DOM 节点上,算出每个元素最终生效的样式。
  2. Layout(布局,也就是 Reflow:根据样式和 DOM 结构,计算每个元素在页面中的几何信息——位置、大小、盒模型。
  3. Paint(绘制,也就是 Repaint:把每个元素的可视外观(颜色、边框、阴影、文字等)转换成一系列绘制指令,光栅化成实际的像素。
  4. Composite(合成):将绘制好的多个图层,按照正确的层叠顺序、位置和变换关系,合并成最终显示在屏幕上的一帧画面。

这四步是有依赖关系的:Style 变化可能引发 Layout,Layout 变化必然引发 Paint(因为几何形状变了,画面肯定要重新画),Paint 之后一定要经过 Composite 才能显示出来。但反过来,并不是每次改动都要从头跑完整个流程——这正是优化的关键所在。

Reflow:几何信息变了,重新计算布局

Reflow(有的浏览器文档里也叫 Layout)指的是浏览器重新计算元素几何属性的过程:宽高、位置、边距等等。

什么情况会触发 Reflow:

  • 改变元素的尺寸(widthheightpaddingborder 等)
  • 改变元素的位置(topleft,或者改变 position 属性本身)
  • 增删 DOM 节点,或者改变文本内容导致排版变化
  • 改变字体大小、字体族
  • 调整浏览器窗口大小
  • 甚至只是读取某些属性(如 offsetWidthscrollTopgetBoundingClientRect()),也可能强制浏览器提前完成一次布局计算,业内通常叫”强制同步布局”或者”布局抖动”(layout thrashing)

Reflow 之所以是三者中开销最大的一个,原因在于它的连锁反应:一个元素的尺寸变化,很可能影响它的兄弟节点、父节点,甚至波及整个文档的布局。比如一个 flex 容器里某个子项的宽度变了,整行甚至整个容器都可能要重新排布。这就是为什么频繁读写布局相关属性、在循环里反复触发 reflow,会让页面明显卡顿。

Repaint:外观变了,但骨架没变

Repaint 指的是元素的几何信息没有变化,但外观发生了变化,需要重新绘制像素。

什么情况会触发 Repaint(但不触发 Reflow):

  • 改变颜色,比如 colorbackground-color
  • 改变 visibility(注意:display: none 会触发 reflow,因为元素直接从文档流里消失了;而 visibility: hidden 只是让元素不可见,不影响布局,所以只触发 repaint)
  • 改变 box-shadowoutlineborder-radius 等纯视觉效果属性

Repaint 的开销比 Reflow 小,因为它不需要重新计算任何几何关系,只需要按照已有的布局信息重新画一遍像素。但如果重绘的区域很大(比如整个可视区域的背景色变化),成本依然不容小觑。

需要特别说明的是:Reflow 之后必然伴随 Repaint,因为元素的形状、位置变了,之前画好的像素自然作废,必须重画。所以严格来说,Reflow 是 Repaint 的”超集触发条件”,但反过来不成立——Repaint 完全可以独立发生,不需要经过 Reflow。

Composite:把画好的图层拼在一起显示出来

Composite 是很多前端开发者最陌生的一环,但它恰恰是性能优化里最值得利用的部分。

现代浏览器在渲染时,会把页面拆分成若干个图层(layer)。哪些元素会被单独提升为一个图层?常见的情况包括:使用了 transformopacity 做动画的元素,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,避免使用 topleftwidthheight 这类会触发 Layout 的属性
  • 对于会频繁执行动画的元素,可以通过 will-change: transform 提前告诉浏览器”这个元素接下来要变化”,让浏览器提前把它提升为独立合成层(但不要滥用,图层太多反而会增加内存开销和层间合成的成本)
  • 避免在循环中交替读写布局属性(比如先设置 style.width,接着又读 offsetWidth),这种写法会强制浏览器插入额外的同步布局计算,业内称为”布局抖动”,是很典型的性能反模式
  • 批量修改 DOM 时,可以先把元素从文档流中”拿出来”(比如设置 display: none,或者用 DocumentFragment 离线操作),改完再放回去,避免每次修改都触发一次全量重排

小结

用一句话总结这三者的关系:Reflow 关心”元素在哪、多大”,Repaint 关心”元素长什么样”,Composite 关心”这些画好的图层怎么拼在一起显示出来”。它们之间是层层递进又并非完全绑定的关系——改布局必然导致重绘和合成,改外观只需要重绘和合成,而如果变化恰好只发生在独立合成层上,浏览器甚至可以跳过布局和绘制,直接在 GPU 上完成合成。

理解这条渲染管线,不是为了背诵”哪些属性触发 reflow”这张表,而是为了在写动画、做交互的时候,能下意识地问自己一句:这个改动,究竟会让浏览器重新算一遍布局,还是只需要在 GPU 上挪一下图层?想清楚这个问题,很多性能问题在写代码的当下就能被避免,而不是等上线后再去 Performance 面板里排查。

© 版权声明

相关文章

暂无评论

none
暂无评论...