在HTML5动画开发场景中,setInterval是最早被广泛使用的帧率控制方案,但它的固定时间间隔回调机制无法适配不同设备的屏幕刷新率,容易出现掉帧、动画不流畅的问题。requestAnimationFrame作为浏览器原生提供的动画专用API,能够和屏幕刷新周期保持同步,从根源上解决帧率不稳定的问题。

setInterval实现动画的局限性
setInterval的工作逻辑是按照设定的固定时间间隔重复执行回调函数,它不会考虑浏览器的渲染节奏和当前页面的可见状态。常见的问题主要有以下几点:
- 无法匹配屏幕刷新率:大多数屏幕的刷新率是60Hz,也就是每16.7ms刷新一次,如果setInterval设置的时间间隔和这个周期不匹配,就会出现帧丢失的情况。
- 页面不可见时仍会执行:当页面被切换到后台时,setInterval的回调依然会运行,造成不必要的性能消耗,甚至可能导致动画恢复后出现逻辑异常。
- 时间间隔不准确:浏览器的事件循环机制会导致setInterval的实际执行时间和设定时间存在偏差,长时间运行后偏差会不断累积。
setInterval实现基础动画示例
下面是一个使用setInterval实现方块移动的简单动画代码:
// 获取画布和上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 方块初始位置
let x = 0;
// 设置定时任务,每16ms执行一次
const timer = setInterval(() => {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新方块位置
x += 2;
// 绘制方块
ctx.fillRect(x, 50, 30, 30);
// 到达边界后停止动画
if (x > canvas.width) {
clearInterval(timer);
}
}, 16);
requestAnimationFrame的核心优势
requestAnimationFrame是浏览器专门为动画场景设计的API,它的回调执行时机和屏幕的刷新周期完全同步,核心优势如下:
- 自动匹配刷新率:回调函数会在每次屏幕刷新前执行,保证动画的每一帧都能被准确渲染,不会出现掉帧问题。
- 页面不可见时暂停执行:当页面处于后台或者最小化状态时,requestAnimationFrame的回调会暂停执行,减少性能消耗。
- 执行时机更合理:它的回调是在浏览器的渲染步骤之前执行,能够避免布局和绘制的重复计算,提升动画性能。
requestAnimationFrame的基础用法
requestAnimationFrame接受一个回调函数作为参数,回调函数执行时会接收一个时间戳参数,表示当前帧的触发时间。调用后会返回一个请求ID,用于后续取消动画。基础使用示例如下:
// 获取画布和上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 方块初始位置
let x = 0;
// 动画请求ID
let animationId = null;
// 动画回调函数
function animate() {
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 更新方块位置
x += 2;
// 绘制方块
ctx.fillRect(x, 50, 30, 30);
// 未到达边界则继续请求下一帧
if (x <= canvas.width) {
animationId = requestAnimationFrame(animate);
}
}
// 启动动画
animationId = requestAnimationFrame(animate);
用requestAnimationFrame替代setInterval的稳帧技巧
1. 基于时间戳计算位移,避免帧率波动影响动画速度
不同设备的屏幕刷新率可能不同,比如有的设备是120Hz,有的是60Hz,如果固定每帧移动固定距离,会导致高刷新率设备上动画速度变快。我们可以通过时间戳计算两帧之间的时间差,动态调整位移量,保证动画速度一致。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let x = 0;
// 记录上一帧的时间戳
let lastTime = 0;
// 动画速度,单位:像素/秒
const speed = 120;
let animationId = null;
function animate(currentTime) {
// 计算时间差,单位:秒
const deltaTime = lastTime ? (currentTime - lastTime) / 1000 : 0;
lastTime = currentTime;
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 根据时间差计算位移,保证速度稳定
x += speed * deltaTime;
// 绘制方块
ctx.fillRect(x, 50, 30, 30);
// 未到达边界则继续动画
if (x <= canvas.width) {
animationId = requestAnimationFrame(animate);
}
}
animationId = requestAnimationFrame(animate);
2. 封装通用的动画循环工具函数
为了更方便地复用requestAnimationFrame的逻辑,可以封装一个通用的动画循环工具,支持自定义更新逻辑和停止条件。
/**
* 通用requestAnimationFrame动画循环工具
* @param {Function} update 每一帧的更新逻辑,接收一个deltaTime参数(时间差,单位秒)
* @param {Function} checkStop 停止条件判断函数,返回true时停止动画
* @returns {Function} 停止动画的函数
*/
function createAnimationLoop(update, checkStop) {
let lastTime = 0;
let animationId = null;
function loop(currentTime) {
const deltaTime = lastTime ? (currentTime - lastTime) / 1000 : 0;
lastTime = currentTime;
// 执行更新逻辑
update(deltaTime);
// 判断是否停止
if (!checkStop()) {
animationId = requestAnimationFrame(loop);
}
}
// 启动动画
animationId = requestAnimationFrame(loop);
// 返回停止函数
return () => {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
};
}
// 使用示例
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let x = 0;
const stopAnimation = createAnimationLoop(
// 更新逻辑
(deltaTime) => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
x += 120 * deltaTime;
ctx.fillRect(x, 50, 30, 30);
},
// 停止条件
() => x > canvas.width
);
3. 处理动画暂停和恢复逻辑
如果需要支持动画的暂停和恢复,只需要记录暂停时的时间戳,恢复时重新计算时间差即可,避免动画恢复后出现跳帧的问题。
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let x = 0;
let lastTime = 0;
let animationId = null;
let isPaused = false;
let pausedTime = 0;
function animate(currentTime) {
if (isPaused) return;
// 处理暂停后恢复的时间差计算
if (pausedTime) {
lastTime = currentTime - pausedTime;
pausedTime = 0;
}
const deltaTime = lastTime ? (currentTime - lastTime) / 1000 : 0;
lastTime = currentTime;
ctx.clearRect(0, 0, canvas.width, canvas.height);
x += 120 * deltaTime;
ctx.fillRect(x, 50, 30, 30);
if (x <= canvas.width) {
animationId = requestAnimationFrame(animate);
}
}
// 启动动画
animationId = requestAnimationFrame(animate);
// 暂停按钮逻辑
document.getElementById('pauseBtn').addEventListener('click', () => {
isPaused = true;
// 记录暂停时的时间差
pausedTime = Date.now() - lastTime;
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
});
// 恢复按钮逻辑
document.getElementById('resumeBtn').addEventListener('click', () => {
isPaused = false;
animationId = requestAnimationFrame(animate);
});
两种方案的适用场景对比
为了更清晰地选择动画实现方案,下面是requestAnimationFrame和setInterval的适用场景对比:
| 对比维度 | requestAnimationFrame | setInterval |
|---|---|---|
| 帧率稳定性 | 高,和屏幕刷新率同步 | 低,固定间隔不匹配刷新率 |
| 性能消耗 | 低,页面不可见时暂停 | 高,页面不可见时仍执行 |
| 适用场景 | 所有需要流畅表现的视觉动画 | 非视觉类的定时任务,不需要和渲染同步的逻辑 |
| 时间精度 | 高,和渲染节奏匹配 | 低,受事件循环影响偏差大 |
注意事项
- requestAnimationFrame的回调执行次数和屏幕刷新率一致,不要在回调中执行过于耗时的逻辑,避免阻塞渲染导致卡顿。
- 如果需要兼容非常老的浏览器(如IE9及以下),需要添加对应的polyfill,或者使用setTimeout模拟降级方案。
- 取消动画时一定要使用
cancelAnimationFrame方法,传入对应的请求ID,避免内存泄漏。
requestAnimationFramesetIntervalHTML5动画稳帧技巧修改时间:2026-06-11 02:24:45