2. 浏览器渲染流水线:JavaScript → Style → Layout → Paint → Composite 全链路解析

很多同学写动画,上来就是 transform: translateX(100px) 加个 transition,觉得完事了。但为什么有的动画掉帧?为什么有的页面滚动像幻灯片?

说白了,你得知道浏览器到底是怎么把代码变成像素的。我当年在做一个 H5 活动页时,一个简单的位移动画在低端机上卡成 PPT,查了半天才发现是触发了 Layout。从那以后,我每次写动画前都会在脑子里过一遍这条流水线。

浏览器渲染流水线,一共五步:JavaScript → Style → Layout → Paint → Composite。每一步都有它的性能开销,咱们一个一个拆开看。

核心结论:动画性能优化的本质,就是尽量让流水线只走后面的步骤,跳过前面的重活。

浏览器渲染流水线(五步走) JavaScript Style Layout Paint Composite 开销:中 开销:低 开销:高 开销:高 开销:低 优化目标:尽量只走 Composite 使用 transform / opacity 动画 → 跳过 Layout 和 Paint 修改 width / height / top / left → 触发 Layout + Paint + Composite

2.1 JavaScript:动画的起点

动画不一定非得用 JS 写,但大部分复杂动画还是离不开 JS。比如用 requestAnimationFrame 驱动、用 Web Animations API、或者用 GSAP 这类库。

JS 阶段的性能开销,主要来自:

  • DOM 操作——增删改查 DOM 节点,每次都会触发后续流程
  • 样式读取——比如 el.offsetHeight,会强制浏览器提前 Layout
  • 长任务——JS 执行超过 50ms,就会卡住主线程,动画直接掉帧

我的习惯:动画循环里尽量别做 DOM 查询。需要的数据提前缓存好。比如 const boxWidth = el.offsetWidth 在循环外面读一次就够了。

2.2 Style:样式计算

JS 改了元素的样式属性后,浏览器要重新计算所有元素的最终样式。这一步叫 Style 计算(也叫 Recalculate Style)。

开销取决于:

  • 选择器复杂度——.a .b .c .d {} 这种嵌套深的选择器,计算量更大
  • 元素数量——页面里 100 个元素和 10000 个元素,计算时间差两个数量级
  • 伪类/伪元素——:nth-child、:hover 这些会额外增加匹配成本

我见过一个项目,CSS 选择器写了八层嵌套,每次样式变化都要算几百毫秒。后来改成 BEM 命名 + 扁平选择器,Style 时间直接降到个位数毫秒。

避坑:不要用 * { box-sizing: border-box } 这种通配选择器去覆盖所有元素。虽然方便,但每次 Style 计算都要遍历整个 DOM 树。用 html { box-sizing: border-box } 然后继承就够了。

2.3 Layout:布局计算(最耗性能的一步)

Style 算完了每个元素的样式,接下来要算它们的位置和大小。这就是 Layout(也叫 Reflow)。

为什么说它最耗性能?因为 Layout 是 全局性的。你改了一个元素的宽度,浏览器可能要重新计算它所有子元素、父元素、甚至兄弟元素的位置。

触发 Layout 的属性 不触发 Layout 的属性
width / height transform
top / left / margin opacity
padding / border filter(部分)
display: none → block will-change
font-size / line-height pointer-events

我曾经在做一个列表动画时,用 left 做位移,结果每次帧都要 Layout 整个列表。换成 transform: translateX() 后,动画直接从 20fps 飙到 60fps。嗯,这就是 Layout 的威力。

2.4 Paint:绘制

Layout 算完了位置,接下来要画像素了。Paint 阶段会把元素绘制成一张张 位图(也叫图层)。

开销取决于:

  • 绘制区域大小——全屏渐变背景 vs 一个小图标,绘制时间差很多
  • 绘制复杂度——border-radius、box-shadow、text-shadow、渐变、SVG 滤镜,这些都会增加绘制时间
  • 重绘区域——改动一个元素,浏览器可能只重绘它所在的矩形区域,但如果元素有 overflow: visible,重绘区域会扩大

关键点:Paint 是在 CPU 上完成的(早期浏览器),现代浏览器会用 GPU 加速部分绘制。但复杂的绘制效果依然会拖慢帧率。

2.5 Composite:合成

最后一步,浏览器把各个图层合成到一起,输出到屏幕上。这一步由 GPU 完成,速度非常快。

Composite 阶段只处理两个事情:

  • 图层的位移、缩放、旋转、透明度——这些操作不涉及重新 Layout 或 Paint
  • 图层的叠加顺序——z-index、opacity 等

所以,如果你用 transform 和 opacity 做动画,浏览器只需要走 JavaScript → Style → Composite,跳过 Layout 和 Paint。这就是为什么它们被称为「性能友好的动画属性」。

我的建议:写动画前先问自己三个问题——

  1. 这个动画能不能用 transform 实现?
  2. 能不能用 opacity 实现?
  3. 如果都不行,能不能用 will-change 提前告诉浏览器我要动它?

这三个问题问完,90% 的性能问题都能避免。

2.6 流水线全链路总结

咱们把五步串起来看:

阶段 做什么 性能开销 优化方向
JavaScript 执行动画逻辑、DOM 操作 中 减少 DOM 操作,避免长任务
Style 计算最终样式 低 扁平选择器,减少元素数量
Layout 计算位置和大小 高 避免触发 Layout 的属性
Paint 绘制位图 高 避免复杂绘制效果
Composite 合成图层到屏幕 低 尽量只走这一步

你想想看,一个动画如果每次帧都要走 Layout + Paint,那开销就是「高 + 高」。如果只走 Composite,开销就是「低」。差距有多大?一个 60fps 的动画,每帧只有 16.7ms 的时间。Layout 随便一算就是 5-10ms,再加上 Paint,帧率直接腰斩。

所以,动画性能优化的核心,就是让流水线尽量只走 Composite。后面几章我们会具体讲怎么做到这一点。