该动什么:合成层 vs 布局/绘制
一帧画面要经过布局 (layout) → 绘制 (paint) → 合成 (composite) 三个阶段。
动画每帧改哪个 CSS 属性,决定了浏览器要从这条管线的哪一层重新算起:动
left / top / width / height / margin 会触发 layout,整条管线从头跑;动
color / box-shadow / background 跳过 layout 但仍要 paint;而动
transform / opacity 只需 composite,布局与绘制的结果可直接复用,且这一步交给
合成器线程 (compositor thread) 处理,不占主线程。下面用同一个左右往返动画做对照:左侧每帧改
left(并故意读 offsetWidth 制造 layout thrashing),右侧用
transform: translateX,实测每帧耗时与估算 fps。
1 · layout
算每个盒子的几何 (位置 / 尺寸)。left/width/margin 改它。
2 · paint
把盒子栅格化成像素图层。color/shadow/bg 改它。
3 · composite
把现成图层挪位 / 调透明度。transform/opacity 只到这。
动 left(触发 layout)
每帧改 left + 读 offsetWidth 强制同步布局
动 transform(只触发 composite)
每帧改 translateX,布局 / 绘制结果复用
el.style.left = …) 再读
几何 (el.offsetWidth),会逼浏览器立刻同步重算布局来回答这次读取,缓存里攒着的批量
更新被反复打断。这是上面 left 一侧故意制造的最坏情形——读写交错地放大 layout 的代价。
哪个属性落在管线的哪一层,决定了它能不能便宜地动。下表是常见动画属性的归属:
| 属性 | 触发的最早阶段 | 每帧代价 |
|---|---|---|
transform / opacity | composite | 低(合成器线程,可不占主线程) |
color / background / box-shadow / border-radius | paint | 中(重新栅格化图层) |
left / top / width / height / margin / padding | layout | 高(几何重排,后续 paint + composite 跟着重跑) |
will-change: transform 提前告诉
浏览器「这个元素将要变换」,促其预先建层、避免动画开始那一刻的建层抖动——但它有显存成本,只标
真正要动的元素,动完可移除。
本页测的是「动什么属性」;当布局确实必须变 (如卡片换位、尺寸切换),用
高级交互 里的 FLIP 技术:先量布局前后的位置差,把变化倒推成一段
纯 transform 动画,从而避开逐帧重排。每帧到底由谁驱动、如何做到帧率无关,见
驱动器与帧。