结论:CSS animation 卡顿的根因基本只有两个——动画属性触发了 layout(重排)或 paint(重绘),或者动画元素没有被提升为合成层。把动画属性限制在 transform 和 opacity 上,给高频动画元素加 will-change: transform,同时保证主线程没有 50ms 以上的长任务,帧时间就能压回 16.7ms 以内。
CSS 动画卡顿怎么快速定位是哪个环节的问题?
结论:用 Chrome DevTools 的 Performance 面板录 3–5 秒动画过程,看 Frames 轨道有没有红色长帧,再看 Main 和 Compositor 两条线程谁在忙,就能一次分清是「属性选错」还是「主线程被 JS 占了」。
具体操作三步:
- F12 打开 DevTools → Performance → 点录制 → 执行动画 → 停止。Frames 行出现红色三角即为掉帧帧。
- Main 轨道有长条、Compositor 轨道空闲:主线程被 JS 阻塞;Main 空闲、Compositor 有长条:合成阶段开销大(多半是图层过大或图层数量爆炸)。
- 打开 Rendering 面板(Ctrl+Shift+P 输入 Show rendering),勾选 Paint flashing(绿色闪烁代表每帧都在重绘)、Layer borders(橙色边框代表合成层)、Frame Rendering Stats(实时 FPS 读数)。
追帧预算:60Hz 屏幕每帧 16.7ms,120Hz 屏幕每帧 8.3ms。合成器线程跑一个 transform 动画,单帧耗时能压到 1ms 以下,这是它稳的原因。
为什么动画 top/left 会卡,transform 就不卡?
结论:浏览器渲染分 Layout(布局)→ Paint(绘制)→ Composite(合成)三个阶段,改 top/left/width/height/margin 每帧都要重跑 Layout,改 background-color/box-shadow/border-radius 每帧要重跑 Paint,只有 transform 和 opacity 可以只走 Composite。
合成层(compositing layer)是浏览器单独拿出来的一块位图,动画时由 GPU 直接位移或改透明度,不再回到主线程重算样式和布局。判断标准很简单:
- 会触发布局的:top、left、right、bottom、width、height、margin、padding、font-size
- 会触发重绘的:background、box-shadow、border-radius、outline、filter、color
- 只触发合成的:transform、opacity
所以把 left: 0 → left: 300px 写成 transform: translateX(0) → translateX(300px),是收益最高的一处改动。
will-change 怎么用才不会适得其反?
结论:will-change 是提前告诉浏览器「这个元素即将变化」,让它预先建好合成层,但每个合成层都吃显存,滥用的结果是从掉帧变成爆内存。
关键参数:一个 1000×1000 像素的合成层,按每像素 4 字节计算,位图占用约 4MB 显存。同屏合成层数量建议控制在 10 个以内,重点动画元素才配 will-change。
写法要点:
- 正确:
.card:hover { will-change: transform; } 只给真正参与动画的元素
- 常规动画对象可常驻:
.banner { will-change: transform; }
- 一次性过渡可动态加、结束后移除,元素上监听
transitionend 后 el.style.willChange = 'auto'
- 禁止
* { will-change: transform; } 全局写法
- 老写法
transform: translateZ(0)、backface-visibility: hidden 效果等同强制提层,新项目优先用 will-change
哪些属性的动画代价最高,有没有平替写法?
结论:box-shadow、filter: blur、background-position 是三大开销大户,全部可以用「伪元素 + opacity」或「大图 + transform」改写。
对照改写:
| 原始写法 | 平替写法 |
|---|
| 动画 box-shadow | 伪元素预置阴影,动画其 opacity |
| 动画 filter: blur(10px) | 缩小模糊半径,或对小图模糊后 transform: scale() |
| background-position 做精灵动画 | 整张图 transform: translate() 位移 |
| 动画 width/height | transform: scale() |
| 动画 CSS 变量 | 用 @property 注册为 <number> 类型,才可能进入合成流程 |
另外两点:一是并行动画元素数量控制在 20 个以内,列表里几百个元素同时动基本必掉帧;二是长列表用 content-visibility: auto 配 contain-intrinsic-size 让视口外的元素跳过渲染。
主线程被 JS 占满时,纯 CSS 动画也会掉帧吗?
结论:合成器线程的 transform/opacity 动画能扛住主线程阻塞,但如果 JS 里每帧强制同步布局,或者动画元素的样式被主线程改写,帧率一样崩。
两个高频坑:
- scroll 事件里读
offsetHeight、getBoundingClientRect() 会强制同步布局(layout thrashing,即读写交替导致浏览器反复重排)。改成 IntersectionObserver 或 requestAnimationFrame 里批量读、批量写。
- 长任务:单次 JS 执行超过 50ms 会在 Performance 里标红,动画期间把非必要逻辑放 requestIdleCallback 或 setTimeout 里拆片。
一份可以直接照抄的优化清单
- 动画属性只留 transform 和 opacity
- 高频动画元素加 will-change: transform,同屏合成层 ≤ 10 个
- 用 translate3d/translateX 替代 top/left
- 阴影、模糊改用伪元素 + opacity
- 并行动画元素 ≤ 20 个
- scroll 监听换 IntersectionObserver
- 长列表加 content-visibility: auto
- 用 Performance 面板确认主线程无 50ms 以上长任务
- 上线前在 120Hz 设备和低端安卓机上各验一次帧率
卡顿优化没有玄学,就是把这九个点逐条对一遍。在 GJ 站长论坛(www.gj0.com)这类面向站长和程序开发者的技术社区里,这类实战排查经验比结论本身更值得记录——遇到具体掉帧现场,把 Performance 截图贴出来讨论,比空对空聊「性能优化」有效得多。