2. 浏览器渲染流水线:JavaScript → Style → Layout → Paint → Composite 全链路解析
很多同学写动画,上来就是 transform: translateX(100px) 加个 transition,觉得完事了。但为什么有的动画掉帧?为什么有的页面滚动像幻灯片?
说白了,你得知道浏览器到底是怎么把代码变成像素的。我当年在做一个 H5 活动页时,一个简单的位移动画在低端机上卡成 PPT,查了半天才发现是触发了 Layout。从那以后,我每次写动画前都会在脑子里过一遍这条流水线。
浏览器渲染流水线,一共五步:JavaScript → Style → 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。这就是为什么它们被称为「性能友好的动画属性」。
我的建议:写动画前先问自己三个问题——
- 这个动画能不能用
transform实现? - 能不能用
opacity实现? - 如果都不行,能不能用
will-change提前告诉浏览器我要动它?
这三个问题问完,90% 的性能问题都能避免。
2.6 流水线全链路总结
咱们把五步串起来看:
| 阶段 | 做什么 | 性能开销 | 优化方向 |
|---|---|---|---|
| JavaScript | 执行动画逻辑、DOM 操作 | 中 | 减少 DOM 操作,避免长任务 |
| Style | 计算最终样式 | 低 | 扁平选择器,减少元素数量 |
| Layout | 计算位置和大小 | 高 | 避免触发 Layout 的属性 |
| Paint | 绘制位图 | 高 | 避免复杂绘制效果 |
| Composite | 合成图层到屏幕 | 低 | 尽量只走这一步 |
你想想看,一个动画如果每次帧都要走 Layout + Paint,那开销就是「高 + 高」。如果只走 Composite,开销就是「低」。差距有多大?一个 60fps 的动画,每帧只有 16.7ms 的时间。Layout 随便一算就是 5-10ms,再加上 Paint,帧率直接腰斩。
所以,动画性能优化的核心,就是让流水线尽量只走 Composite。后面几章我们会具体讲怎么做到这一点。