requestAnimationFrame比setTimeout更适合动画,因其自动对齐屏幕刷新节奏(通常60fps)、页面不可见时自动暂停省电、避免掉帧卡顿;需递归调用、用时间戳计算增量、显式调用cancelAnimationFrame取消。

requestAnimationFrame 为什么比 setTimeout 更适合动画
因为 requestAnimationFrame 会自动对齐浏览器的重绘节奏(通常是每秒 60 帧),而 setTimeout 或 setInterval 的执行时机不可控,容易出现跳帧、卡顿或掉帧。浏览器在页面不可见时还会自动暂停 requestAnimationFrame,省电又合理。
常见错误现象:用 setInterval(fn, 16) 模拟 60fps,但实际回调可能堆积、延迟,尤其在 CPU 压力大或 tab 切走再切回时,动画突然“往前猛跳”一段。
使用场景:滚动视差、拖拽反馈、路径动画、Canvas 绘图循环等对帧率敏感的交互。
基础写法与递归调用要点
它不是一次性调用,必须手动递归触发下一次,否则只执行一帧。
- 每次回调函数执行完,需显式调用
requestAnimationFrame才能继续 - 回调函数接收一个参数:当前帧的时间戳(
DOMHighResTimeStamp),建议用它计算增量,而不是依赖Date.now() - 避免在回调里做耗时操作(如大量 DOM 查询、复杂计算),否则会挤占渲染时间,导致掉帧
示例:
function animate(timestamp) {
// 用 timestamp 计算 delta,而非 Date.now()
if (!lastTimestamp) lastTimestamp = timestamp;
const delta = timestamp - lastTimestamp;
lastTimestamp = timestamp;
<p>// 更新状态、修改样式...
element.style.transform = <code>translateX(${position}px)</code>;</p><p>// 关键:递归调用
requestAnimationFrame(animate);
}
let lastTimestamp = 0;
requestAnimationFrame(animate);
如何正确取消动画循环
很多人只记得开始,忘了清理——这会导致内存泄漏和意外持续执行。
requestAnimationFrame 返回一个 ID(类似 setTimeout),但取消必须用 cancelAnimationFrame,不能靠 return 或 break。
- 保存上一次调用返回的 ID:
let rafId = requestAnimationFrame(...) - 取消时调用
cancelAnimationFrame(rafId),之后把rafId设为null避免重复取消 - 在组件卸载、元素移除、条件满足(如动画到终点)时主动取消
错误写法:return 出去就以为停了;或者没存 ID,根本没法取消。
和 CSS 动画/transition 冲突时怎么办
直接改 element.style.xxx 会覆盖 CSS 中的 transition 或 @keyframes,导致动画中断或行为异常。
更稳妥的做法是:用 transform 和 opacity 这类可被 GPU 加速的属性,且避免同时用 JS 修改和 CSS 定义同一属性的动画。
- 优先用
transform替代left/top,减少重排(reflow) - 如果需要和 CSS 动画协同,可用
animation-play-state: paused暂停,再用 JS 控制关键帧进度(配合getAnimations()) - 调试时注意:Chrome DevTools 的 “Rendering” 面板勾选 “FPS Meter”,实时看是否真跑满 60fps
真正难的不是写对第一帧,而是让成百上千帧都稳定在 16ms 内完成——这取决于你每帧里干了什么,而不是用没用 requestAnimationFrame。

















