requestAnimationFrame 比 setInterval 更适合动画,因其自动对齐屏幕刷新率(如60fps)、后台暂停省资源、回调精准触发于绘制前、支持精确启停;需递归调用、用时间戳计算deltaTime避免帧率依赖,并避免DOM读写和CSS动画冲突。

requestAnimationFrame 为什么比 setInterval 更适合做动画
因为 requestAnimationFrame 会自动对齐浏览器的刷新节奏(通常是 60fps),而 setInterval 是纯 JS 计时器,不受屏幕刷新率约束,容易出现掉帧、卡顿或“撕裂”感。更关键的是:页面切到后台时,requestAnimationFrame 会被浏览器暂停,setInterval 却照常执行——这不仅浪费 CPU,还可能让动画状态错乱。
- 用
setInterval(fn, 16)模拟 60fps?实际执行间隔可能漂移,尤其在 JS 主线程繁忙时 -
requestAnimationFrame的回调总在下一帧绘制前触发,天然适配渲染管线 - 它返回一个 ID,可用
cancelAnimationFrame(id)精确控制停帧,比clearInterval更可靠
最简 requestAnimationFrame 动画循环写法
别套用 setInterval 的思维去“轮询”,requestAnimationFrame 是递归驱动的。核心就三步:定义动画函数 → 在函数末尾调用自己 → 启动一次。
let animationId = null;
function animate() {
// 更新状态:比如 this.x += this.speed
// 渲染:比如 ctx.fillRect(this.x, this.y, 10, 10)
animationId = requestAnimationFrame(animate);
}
animationId = requestAnimationFrame(animate); // 启动- 务必把
requestAnimationFrame(animate)放在函数末尾,不是开头 - 启动只需调用一次
requestAnimationFrame(animate),不是animate() - 想暂停?直接
cancelAnimationFrame(animationId);恢复?再调一次requestAnimationFrame(animate)
如何处理动画时间差(delta time)避免速度受帧率影响
直接用 requestAnimationFrame 不传参,你拿到的是当前帧的时间戳(DOMHighResTimeStamp),不是固定间隔。如果每帧都加固定值(如 x += 2),那在 30fps 设备上就会慢一半。
正确做法是记录上一帧时间,算出本次耗时(delta),再按比例更新位移:
立即学习“前端免费学习笔记(深入)”;
let lastTime = 0;
function animate(currentTime) {
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
// 以每秒 120px 为目标速度
this.x += (120 / 1000) * deltaTime;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
-
currentTime是高精度时间戳(单位毫秒),比Date.now()更准 - 别用
performance.now()再取一次——requestAnimationFrame的参数就是它 - deltaTime 为 0 是可能的(连帧),需容忍,不用特殊处理
常见踩坑:清除不干净、重复启动、与 CSS 动画冲突
很多人写完发现动画停不掉、越跑越快,或者和 transform: translateX() 一起用时抽风——问题往往不在 API 本身。
- 忘记存
requestAnimationFrame返回的 ID,导致无法cancelAnimationFrame - 多次调用启动逻辑(比如按钮点两次),造成多个嵌套动画循环同时运行
- JS 修改
left/top,又用 CSS transition 控制同一属性:两者打架,浏览器可能丢帧或跳变 - 在
animate里频繁读写 DOM(如反复查offsetTop),触发强制同步布局(layout thrashing)
稳帧不是靠“调得更密”,而是让每一帧的计算轻、渲染快、不打断浏览器流程。真正难的不是写循环,是把状态更新、插值、绘制这几步拆干净,且避开 layout 和 paint 的雷区。











