问题:
之前用setTimeout实现了一个弹幕动画,动画跑着跑着画面越来越快,这是为什么?怎样解决?
问题原因分析
当使用 setTimeout
实现弹幕动画时出现越来越快的情况,通常由以下几个原因导致:
-
时间累积误差:
setTimeout
不能保证精确的时间间隔,每次执行可能有微小延迟,这些延迟会累积
-
回调执行时间:动画逻辑本身的执行时间会影响下一次调用的时机
-
事件循环机制:
setTimeout
受浏览器事件循环影响,优先级低于渲染等任务
-
未考虑帧同步:没有与屏幕刷新率同步,导致动画速度不稳定
怎样解决
- 对于现代浏览器,始终优先使用 requestAnimationFrame
- 如需支持旧浏览器,可使用以下polyfill:
window.requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
- 对于需要精确时间控制的动画,使用基于时间戳的计算方式
- 避免在动画中执行耗时操作,保持每帧执行时间在3-4ms以内
为什么requestAnimationFrame可以解决这个问题
1. 与浏览器刷新率硬同步
-
setTimeout
的问题:
即便设置为 16ms
(模拟60fps),由于 JavaScript 单线程特性,setTimeout
回调可能被其他任务阻塞,导致实际执行间隔不稳定(可能变成 17ms、20ms 甚至更长)。这些延迟累积会让动画越来越快(因为代码中通常用固定步长位移,而非基于时间计算)。
-
requestAnimationFrame 的解决方案:
requestAnimationFrame 直接绑定到浏览器的渲染周期,在每次屏幕刷新前执行(通常严格保持 16.7ms/帧,60Hz 屏幕)。浏览器会智能合并 requestAnimationFrame 回调,确保动画与硬件刷新率同步,避免时间漂移。
2. 基于时间戳的自动补偿
-
requestAnimationFrame 的回调函数会自动接收一个 timestamp
参数(高精度时间戳),开发者可以用它计算真实的时间差,实现帧率无关的动画:
let lastTime;
function animate(timestamp) {
if (!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime; // 实际经过的时间
lastTime = timestamp;
// 根据 deltaTime 计算位移(避免固定步长导致的加速)
element.style.left = (element.offsetLeft + speed * (deltaTime / 16.67)) + 'px';
requestAnimationFrame(animate);
}
即使某帧延迟,deltaTime
也会按实际时间调整位移量,保持速度恒定。
3. 后台自动休眠
-
setTimeout
的问题:
即使页面隐藏,setTimeout
仍会继续执行,导致不必要的计算和电量消耗。
-
requestAnimationFrame 的解决方案:
当页面不可见(如切换标签页或最小化),浏览器会自动暂停 rAF 回调,恢复可见时继续执行。这既节省资源,又避免了不可见时的动画逻辑堆积(堆积的 setTimeout
回调会在页面恢复时集中执行,导致动画瞬间跳跃)。
4. 浏览器级优化
- requestAnimationFrame 的优先级高于
setTimeout
,浏览器会优先调度动画相关的渲染任务。
- 对连续多个 requestAnimationFrame 调用,浏览器会合并处理,避免冗余计算(例如快速连续调用
requestAnimationFrame
时,浏览器可能只执行一次回调)。
对比代码示例
❌ setTimeout
的问题实现(会加速)
let pos = 0;
function move() {
pos += 2; // 固定步长,时间漂移时必然加速
element.style.left = pos + 'px';
setTimeout(move, 16); // 无法保证严格 16ms
}
move();
✅ requestAnimationFrame
的正确实现
let pos = 0, lastTime;
function move(timestamp) {
if (!lastTime) lastTime = timestamp;
const deltaTime = timestamp - lastTime;
lastTime = timestamp;
pos += 2 * (deltaTime / 16.67); // 根据实际时间调整步长
element.style.left = pos + 'px';
requestAnimationFrame(move);
}
requestAnimationFrame(move);
总结表
问题根源 |
setTimeout 的表现 |
requestAnimationFrame 的解决方案 |
时间不同步 |
延迟累积导致动画加速 |
严格同步屏幕刷新率 |
后台资源浪费 |
隐藏页面仍执行动画 |
自动暂停回调 |
位移计算不精确 |
固定步长导致速度不稳定 |
基于时间戳动态计算位移 |
优先级低 |
可能被其他任务阻塞 |
浏览器优先调度 |
结论:requestAnimationFrame
是专为动画设计的 API,从底层解决了 setTimeout
的时间同步问题,是现代 Web 动画的首选方案。
目前端动画实现方案概览
目前前端实现动画主要有以下几种方案:
-
CSS 动画/过渡:
transition
、animation
-
JavaScript 定时器:
setTimeout
、setInterval
-
requestAnimationFrame:浏览器专为动画提供的API
-
Web Animations API:较新的原生动画API
-
动画库:GSAP、Anime.js、Velocity.js等
-
Canvas/SVG 动画:通过绘图API实现
官方解释
window.requestAnimationFrame()
方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。
对回调函数的调用频率通常与显示器的刷新率相匹配。虽然 75hz、120hz 和 144hz 也被广泛使用,但是最常见的刷新率还是 60hz(每秒 60 个周期/帧)。为了提高性能和电池寿命,大多数浏览器都会暂停在后台选项卡或者隐藏的 <iframe>
中运行的 requestAnimationFrame()
。
原理
-
与浏览器刷新率同步:通常以 60fps (每16.7ms执行一次) 的速率执行,与屏幕刷新率保持一致
-
自动暂停:当页面不可见或最小化时,动画会自动暂停,节省 CPU/GPU 资源
-
浏览器优化:浏览器会合并多个 rAF 请求,进行统一处理
优势
对比 setTimeout/setInterval
-
更精确的时机控制:与屏幕刷新同步,避免丢帧
-
更高效:浏览器会优化执行,页面不可见时暂停
-
更省电:非活动页面自动停止动画
对比 CSS 动画
-
更灵活的控制:可以处理复杂逻辑和交互
-
更丰富的效果:可以实现 CSS 难以表达的动画
-
更好的性能监控:可以精确控制每一帧
各方案对比表格
对比维度 |
CSS 动画/过渡 |
JavaScript 定时器 |
requestAnimationFrame |
Web Animations API |
GSAP等动画库 |
实现复杂度 |
简单 |
中等 |
中等 |
中等 |
简单(API友好) |
控制精度 |
低(关键帧之间) |
高 |
最高 |
高 |
高 |
性能 |
高(浏览器优化) |
低(可能丢帧) |
最高(与渲染同步) |
高 |
高(优化过) |
资源消耗 |
低 |
中-高 |
低 |
中 |
中(库体积) |
兼容性 |
好(需前缀) |
极好 |
好(IE10+) |
一般(较新浏览器) |
好(兼容处理) |
适用场景 |
简单UI动画 |
简单定时任务 |
复杂交互动画 |
复杂动画 |
专业复杂动画 |
能否暂停/继续 |
可以 |
可以 |
可以 |
可以 |
可以 |
时间控制 |
有限 |
精确 |
非常精确 |
精确 |
非常精确 |
GPU加速 |
是 |
否 |
是 |
是 |
是 |
代码示例 |
[见下方] |
[见下方] |
[见下方] |
[见下方] |
[略] |