要让 DOM 动画稳定跑在 60FPS,核心是用 requestAnimationFrame(rAF)绑定浏览器刷新节奏;它依设备刷新率自动调度、提供高精度时间戳、避免强制同步布局,并应配合 transform/opacity 和时间驱动状态更新。

要让 DOM 动画稳定跑在 60FPS,核心不是“手动控制帧率”,而是**让动画逻辑严格绑定浏览器的刷新节奏**,而 requestAnimationFrame(简称 rAF)正是为此设计的原生 API。它本身不保证 60FPS,但能确保你的动画代码在**下一帧绘制前执行**,避免丢帧、卡顿和强制同步布局(layout thrashing)。
理解 rAF 的本质:不是定时器,而是绘制协调器
rAF 不是 setInterval 的替代品。它不按固定毫秒间隔调用,而是由浏览器根据当前设备刷新率(如 60Hz、90Hz、120Hz)统一调度,并自动在每帧的“重绘前”触发回调。这意味着:
- 在 60Hz 屏幕上,rAF 回调理想情况下每 ~16.67ms 被调用一次;在 120Hz 屏幕上则约每 ~8.33ms 一次——你无需硬编码 16.67
- 当标签页不可见、系统负载高或动画被暂停时,rAF 会自动降频甚至暂停,节省资源
- 它天然与 CSS 合成层、GPU 渲染管线对齐,比 setTimeout/setInterval 更易获得流畅表现
正确使用 rAF 实现可预测的动画循环
一个健壮的动画循环需包含三要素:时间戳驱动、状态更新、DOM 应用。避免直接操作 style 或频繁读写 offsetTop/offsetLeft 等触发回流的属性。
-
始终使用 rAF 提供的时间戳:回调函数第一个参数是高精度时间戳(
DOMHighResTimeStamp),用于计算真实经过时间(delta),而非依赖setInterval的假定间隔 -
分离“更新逻辑”与“渲染逻辑”:先计算位置/透明度等状态,再批量应用到 DOM;避免边算边读(如
el.offsetTop)导致强制同步布局 - 用 CSS transform 和 opacity 做动画:这两者可被 GPU 加速且不触发 layout/paint,比 top/left/width 等安全得多
示例结构:
let startTime = null;<br>function animate(timestamp) {<br> if (!startTime) startTime = timestamp;<br> const delta = timestamp - startTime;<br><br> // 1. 更新状态(纯计算)<br> const progress = Math.min(delta / 2000, 1); // 2s 动画<br> const x = easeOutCubic(progress) * 300; // 缓动函数计算位置<br><br> // 2. 渲染(仅写入,不读取)<br> el.style.transform = `translateX(${x}px)`;<br><br> // 3. 继续循环(除非完成)<br> if (progress < 1) requestAnimationFrame(animate);<br>}<br>requestAnimationFrame(animate);避免常见陷阱:哪些操作会拖垮 FPS
rAF 只负责调度时机,若回调内执行耗时操作,仍会导致掉帧。以下行为极易引发 30FPS 甚至更低:
-
在 rAF 回调中读取 layout 属性:如
clientWidth、offsetTop、getComputedStyle(el).height—— 这会强制浏览器同步计算样式和布局,阻塞渲染流水线 -
频繁创建/销毁 DOM 节点:动画中 append/remove 元素应尽量避免;可用
display: none或visibility: hidden替代 - 未节流的事件监听器联动动画:如 scroll/mousemove 触发 rAF,但未做防抖或合并处理,可能造成 rAF 队列堆积
- 过度使用 box-shadow、filter、canvas 复杂绘制:这些虽不触发 layout,但增加 paint 和 composite 成本,尤其在低端设备上
进阶:配合 IntersectionObserver 与 will-change 提升稳定性
真正实现“始终 60FPS”,还需结合浏览器优化机制:
-
对视口外元素暂停动画:用
IntersectionObserver监听元素是否可见,不可见时取消 rAF,进入时重新启动 -
提前告知浏览器哪些元素将动画:对即将运行动画的元素设置
will-change: transform(注意:仅对短期动画使用,避免滥用导致内存开销) -
启用 CSS 合成层隔离:给动画元素加
transform: translateZ(0)或opacity: 0.99(需谨慎),促使浏览器为其创建独立图层,减少重绘范围
不复杂,但容易忽略。关键在于信任 rAF 的调度能力,把精力放在精简每帧工作量、规避强制重排重绘上。


















