1. 动画性能基础:理解60fps与16ms原则
做前端动画优化,第一个绕不开的数字就是 60fps。为什么是60?说白了,人眼和大脑的视觉暂留效应,在每秒60帧的刷新率下,画面看起来就是「流畅」的。低于这个数,人眼就能感知到卡顿。
我刚开始做移动端H5的时候,总觉得60fps是个理论值,差个几帧无所谓。直到有一次,产品经理拿着iPhone在我面前划一个列表动画,说「你看,这掉帧了」。嗯,从那以后,我再也不敢小看这60帧了。
60fps 与 16ms 原则
60fps意味着什么?每一帧的生成时间只有:
1000ms ÷ 60 ≈ 16.67ms
也就是说,浏览器要在 16毫秒 内完成从JS执行到像素渲染的全过程。超过这个时间,下一帧就会延迟,造成视觉上的卡顿。
核心原则:每一帧的渲染总耗时必须 ≤ 16ms。超过16ms,就是掉帧。
我个人的习惯是,把16ms拆成两部分:
- 10ms 留给JS逻辑和样式计算
- 6ms 留给布局、绘制和合成
为什么这么分?因为浏览器本身也有开销,留点余量更安全。你想想看,如果JS执行就占了14ms,那布局和绘制基本没时间了,卡顿是必然的。
浏览器渲染流水线
要理解卡顿,必须先搞清楚浏览器是怎么把代码变成像素的。我画了一张流程图,帮你理清这条流水线:
这条流水线,每个阶段都有它的职责:
| 阶段 | 做了什么 | 典型耗时 |
|---|---|---|
| JavaScript | 执行JS逻辑、DOM操作、事件处理、动画计算 | 2~10ms |
| Style | 计算匹配的CSS规则,生成每个元素的最终样式 | 1~3ms |
| Layout | 计算元素的位置和尺寸(重排) | 2~5ms |
| Paint | 绘制像素,生成位图(重绘) | 2~5ms |
| Composite | 将多个图层合成到屏幕上 | 1~2ms |
我的经验:大多数性能瓶颈出在 Layout 和 Paint 阶段。如果你发现动画卡顿,先打开 DevTools 的 Performance 面板,看看是不是 Layout 耗时特别长。我曾经在一个列表动画里,因为用了 top/left 做位移,导致每次都要重新布局,帧率直接掉到30fps。换成 transform: translate() 后,Layout 阶段直接消失了,帧率回到60fps。
什么是卡顿与掉帧
这两个词经常被混用,但严格来说:
- 卡顿(Jank):用户感知到的画面不流畅、抖动、跳跃。这是主观体验。
- 掉帧(Frame Drop):浏览器没能在16ms内完成一帧的渲染,导致那一帧被跳过。这是客观指标。
掉帧是原因,卡顿是结果。一次掉帧可能不明显,但如果连续掉帧,用户就会觉得「这页面好卡」。
注意:不是所有掉帧都能被用户感知。偶尔掉一帧(比如1秒内掉1帧),人眼基本看不出来。但如果1秒内掉3~5帧,卡顿感就很明显了。我一般用 Chrome DevTools 的 FPS 计数器来监控,低于50fps就要警惕。
常见的掉帧原因
根据我的项目经验,掉帧通常来自这几个地方:
- JS执行时间过长——比如在
requestAnimationFrame里做了大量计算或DOM操作。 - 强制同步布局(Forced Reflow)——在JS里先改了样式,又立即读取布局属性,导致浏览器被迫提前执行Layout。
- 大面积重绘——比如改变了整个页面的背景色,或者用了
box-shadow这种高开销属性。 - 图层过多——每个
will-change或translate3d都会创建新图层,图层太多会拖慢合成。 - 内存泄漏——DOM节点只增不减,垃圾回收频繁触发,导致JS执行间歇性卡顿。
一句话总结:动画性能优化的本质,就是让每一帧的渲染流水线跑在16ms以内。能跳过Layout就跳过,能跳过Paint就跳过,实在不行就减少绘制面积。