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 Style Layout Paint Composite 每一帧,浏览器都按这个顺序执行。跳过某些阶段,性能就会提升。 比如只改变 transform/opacity,可以跳过 Layout 和 Paint。 每一帧的时间分配(理想情况 ≤ 16ms) JS 执行 ~6ms Style ~2ms Layout ~3ms Paint ~3ms Composite ~2ms 16ms 红线 如果总耗时 > 16ms → 掉帧 → 用户感知到卡顿 优化目标:让每一帧都跑在 16ms 以内

这条流水线,每个阶段都有它的职责:

阶段 做了什么 典型耗时
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就要警惕。

常见的掉帧原因

根据我的项目经验,掉帧通常来自这几个地方:

  1. JS执行时间过长——比如在 requestAnimationFrame 里做了大量计算或DOM操作。
  2. 强制同步布局(Forced Reflow)——在JS里先改了样式,又立即读取布局属性,导致浏览器被迫提前执行Layout。
  3. 大面积重绘——比如改变了整个页面的背景色,或者用了 box-shadow 这种高开销属性。
  4. 图层过多——每个 will-change 或 translate3d 都会创建新图层,图层太多会拖慢合成。
  5. 内存泄漏——DOM节点只增不减,垃圾回收频繁触发,导致JS执行间歇性卡顿。

一句话总结:动画性能优化的本质,就是让每一帧的渲染流水线跑在16ms以内。能跳过Layout就跳过,能跳过Paint就跳过,实在不行就减少绘制面积。