js requestAnimationFrame
动画掉帧?别再只用 setInterval 了,requestAnimationFrame 才是正解
做前端开发久了,肯定遇到过这种尴尬:精心调优的加载动画或拖拽效果,在 Mac 上丝滑无比,一旦放到旧款安卓手机或者高分屏上,立马掉帧严重,甚至出现画面撕裂。排查半天才发现,问题不出在算法复杂度,而是基础动画逻辑还在用 setTimeout 或 setInterval。
这就像是开车,定时任务好比是你自己踩着油门不管车速,浏览器重绘却像限速路标。两者步调不一致,必然卡顿。而 requestAnimationFrame(简称 rAF)的核心价值,就在于它主动告诉浏览器:“我要开始干活了,请在下一次重绘前调用我。”
为什么它更顺滑?
浏览器的渲染机制本质是“帧”。大多数屏幕刷新率是 60Hz,意味着每秒钟有 60 次机会更新画面,每次间隔约 16.6 毫秒。
传统的定时器完全无法感知这个节奏。如果 setInterval 设置的间隔是 10 毫秒,浏览器还没完成上一帧渲染,你的回调就已经准备好了,导致浏览器被迫跳过某些帧或合并操作;反之如果设为 20 毫秒,又会出现画面停顿。
使用 requestAnimationFrame 时,浏览器会自动调整回调函数的调用频率,确保在每个垂直同步信号之前执行。这不仅保证了动画流畅度,还能自动适配高刷屏设备。
实际开发中的坑
很多教程只教你怎么启动动画,却很少提如何安全地停止,这也是新手容易埋雷的地方。
想象一下,你在写一个弹窗动画组件。用户点击打开,触发了 rAF 循环。这时候用户迅速关闭弹窗并跳转到了新页面。如果不手动清理,之前的动画回调依然会在后台继续运行,直到被垃圾回收机制强制终止。这不仅浪费 CPU 资源,严重时还会引发内存泄漏。
正确的做法是持有回调 ID,并在组件销毁时清除。看下面这段示例代码:
let animationId = null;
function startAnimation() {
if (animationId) return; // 防止重复启动
const animate = (time) => {
// 执行具体的样式更新逻辑
element.style.transform = `translateX(${position}px)`;
position++;
if (position < 100) {
animationId = requestAnimationFrame(animate);
} else {
cancelAnimationFrame(animationId); // 达到目标后手动结束
animationId = null;
}
};
animationId = requestAnimationFrame(animate);
}
function stopAnimation() {
if (animationId) {
cancelAnimationFrame(animationId); // 关键步骤:及时清理
animationId = null;
}
}
这里有个细节值得注意:必须在下次请求时传递回调,而不是直接把函数扔给浏览器。这样可以保证你有权随时通过返回的 ID 进行干预。
另一个隐藏优势:省电模式
除了流畅度,rAF 还有一个常被忽略的特性:当标签页被切换至后台最小化时,浏览器会自动暂停它的执行。
相比之下,如果你使用 setInterval,哪怕用户把页面最小化了,计时器依然在后台跑,白白消耗电量,导致笔记本风扇狂转。对于移动端 H5 页面或长时间运行的 Web 应用,这一点至关重要。利用浏览器的这一特性,本质上是在帮用户体验减负。
避免滥用
虽然 rAF 很强大,但也不能无脑用在所有场景。如果你的业务逻辑纯粹是计算数据而不涉及 DOM 更新或 Canvas 绘制,直接用普通函数即可。毕竟调用 rAF 本身也有一点微小的开销。
另外,在处理滚动监听等高频率事件时,不要直接绑在 scroll 事件里调用 rAF,因为滚动触发频率极高,可能会导致回调堆积。建议配合节流(throttle)思想,只在需要计算布局或视觉效果时才启用帧动画。
结语
技术选型没有绝对的高低,只有合不合适。但在涉及视觉反馈和交互体验的领域,requestAnimationFrame 已经逐渐成为行业标准。它不再是可选项,而是性能优化的底线。
从简单的进度条到复杂的视差滚动,掌握它的原理与清理机制,能让你的代码在保持流畅的同时,更加健壮节能。下一次写动画时,不妨试着放下定时器,让浏览器牵着你的手走。


还没有评论,来说两句吧...