问题:
之前用setTimeout实现了一个弹幕动画,动画跑着跑着画面越来越快,这是为什么?怎样解决?
问题原因分析
当使用 setTimeout
实现弹幕动画时出现越来越快的情况,通常由以下几个原因导致:
- 时间累积误差 :
setTimeout
不能保证精确的时间间隔,每次执行可能有微小延迟,这些延迟会累积 - 回调执行时间:动画逻辑本身的执行时间会影响下一次调用的时机
- 事件循环机制 :
setTimeout
受浏览器事件循环影响,优先级低于渲染等任务 - 未考虑帧同步:没有与屏幕刷新率同步,导致动画速度不稳定
怎样解决
- 对于现代浏览器,始终优先使用 requestAnimationFrame
- 如需支持旧浏览器,可使用以下polyfill:
javascript
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
参数(高精度时间戳),开发者可以用它计算真实的时间差,实现帧率无关的动画:inilet 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
的问题实现(会加速)
ini
let pos = 0;
function move() {
pos += 2; // 固定步长,时间漂移时必然加速
element.style.left = pos + 'px';
setTimeout(move, 16); // 无法保证严格 16ms
}
move();
✅ requestAnimationFrame
的正确实现
ini
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实现
requestAnimationFrame 详解
官方解释
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加速 | 是 | 否 | 是 | 是 | 是 |
代码示例 | [见下方] | [见下方] | [见下方] | [见下方] | [略] |