CSS 动画性能 transform 与 layout 的代价
灯下列维
7 分钟阅读
CSS 动画性能 transform 与 layout 的代价
动画卡顿的根源几乎都指向同一件事:每一帧触发了 layout 或 paint。transform 与 opacity 之所以顺滑,是因为它们能走合成线程,绕开主线程的布局计算。理解渲染管线,比背"用 transform 不用 left"这句结论更有用——因为不是所有 transform 都一定快。
渲染管线:一帧是怎么画出来的
浏览器每帧大致经历:JS 执行 -> 样式计算 -> 布局(layout) -> 绘制(paint) -> 合成(composite)。layout 和 paint 都在主线程,耗时且容易阻塞;composite 多在合成线程,与主线程解耦。动画的流畅度,取决于能否把工作压到 composite 阶段。当主线程被长任务占用时,即便动画本身只改 transform,也会因为"这帧的 JS 没跑完"而掉帧——所以动画性能是全局问题,不只看改了哪个属性。
transform / opacity 走合成
位移、缩放、旋转用 transform,透明度用 opacity,这两个属性在不触发 layout 和 paint 的情况下直接进合成层:
.card {
transition: transform 0.2s ease;
}
.card:hover {
transform: translateX(10px) scale(1.05);
}
相比之下,改 left、top、width 会触发 layout,因为浏览器得重新计算元素几何位置,再重绘、再合成,开销大一个量级。但注意:transform 只是"通常"走合成,若元素同时有 box-shadow 或 border-radius 在动画期间变化,仍会触发 paint,抵消部分收益。
哪些属性会触发 layout
| 改动属性 |
触发阶段 |
代价 |
| left / top / width / height |
layout |
高 |
| margin / padding |
layout |
高 |
| font-size / line-height |
layout |
高 |
| background-color |
paint |
中 |
| box-shadow |
paint |
中 |
| transform / opacity |
composite |
低 |
经验上,能用 transform 表达的运动(位移、缩放、旋转)就别用定位属性去"模拟移动"。但 transform 也有边界:它不影响文档流,所以动画中的元素不会把兄弟元素挤开,若业务需要"推开其他元素"的位移效果,反而得接受 layout 代价或改用别的技术。
will-change 的双刃
will-change: transform 能提示浏览器提前把元素提升为独立合成层,减少动画开始时的图层切换抖动:
.slide { will-change: transform; }
滥用会反噬:每个提升的层都占显存,层过多反而让合成变慢、内存吃紧,尤其在低端移动设备上。动画结束应移除该声明,或只加在确实会动的元素上。边界:will-change 是"提示"不是"命令",浏览器可能忽略;也不要对大量列表项同时设 will-change,会瞬间创建成百上千图层拖垮整机。
一个对比实例
假设要让一个元素从左侧滑入。错误写法是用 @keyframes 改 left:每一帧浏览器都要重新 layout、paint、composite,长列表里会明显掉帧。正确写法是初始 transform: translateX(-100%) 过渡到 translateX(0),全程只走 composite。两者视觉结果一致,但性能差距在低端机上能拉开数倍。这个例子说明:优化动画不是"换属性名",而是理解属性落在渲染管线的哪一段。这个差别肉眼难辨,但帧率曲线会如实反映,调试时录一帧就能看明白。
移动端的额外注意
移动设备 GPU 和内存都更紧张,合成层不是免费午餐。大量同时播放的动画(如列表项入场)会瞬间创建许多图层,触发显存压力甚至被系统回收图层导致闪白。实践上应控制同屏动画数量,入场动画结束后及时移除 will-change,并对不可见区域的动画做暂停(用 IntersectionObserver 感知可见性)。另外,部分移动浏览器对 position: fixed 元素叠加 transform 会有渲染异常,动画元素要避开这类组合。
调试时还能用开发工具的图层面板,确认元素是否真的提升为独立层。如果发现改了 transform 仍频繁 paint,检查是否有 filter、clip-path 这类也会触发重绘的属性在动画期间变化。合成层不是银弹,它只解决 layout/paint 的重复开销,主线程的 JS 长任务照样会卡住整帧。所以动画优化要和"减少主线程长任务"一起看,单改 CSS 未必够。
验证手段
打开浏览器性能面板录制滚动或动画,看每帧是否有大面积的紫色(layout)或绿色(paint)长条;理想动画帧里应当主要是合成层的黄色。也可用图层可视化工具确认元素是否真的独立成层。不要凭感觉优化,先录制一帧看看瓶颈到底在哪。
几个边界
- 动画元素若带有
border-radius 或 box-shadow 变化,仍会触发 paint,抵消 transform 的部分收益。
- 合成层提升后,层与层之间的重叠区域重绘成本可能上升,并非"提层就快"。
- 低端机对合成层数量敏感,移动端应克制使用 will-change。
- 用
requestAnimationFrame 驱动 JS 动画时,要把逻辑放在 rAF 回调里对齐帧节奏,而不是用 setInterval,否则容易和渲染不同步。
经验上,动画优化遵循"先量再改":先确认瓶颈在 layout/paint 还是主线程长任务,再决定是用 transform、提层还是拆任务。盲目加 will-change 经常是负优化。
浏览 0
点赞 0
评论 0