前言:从 60fps 的动画说起
想要实现丝滑流畅的 60fps 动画,或者在单线程 JavaScript 中实现真正的并行计算,关键在于理解事件循环的高阶应用。在 JavaScript 中,常见的动画实现方式主要有三种,它们的性能表现差异很大。
使用 setInterval(不推荐)
function animateWithSetInterval() {
setInterval(() => {
updateAnimation();
renderFrame();
}, 16.67);
}
上述代码试图达到 60fps(1000/60 ≈ 16.67ms),但定时器并不精确,容易导致丢帧或过度绘制,资源浪费严重。
递归 setTimeout
function animateWithSetTimeout() {
function loop() {
updateAnimation();
renderFrame();
setTimeout(loop, 16.67);
}
loop();
}
这种方式比 setInterval 稍好,因为它允许在每次循环前重置计时器,但仍可能和屏幕刷新不同步,造成画面撕裂。
使用 requestAnimationFrame(推荐)
function animateWithRAF() {
function loop(timestamp) {
updateAnimation(timestamp);
renderFrame();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
这是目前浏览器推荐的方案。优势在于它能自动匹配屏幕刷新率,节省资源,避免不必要的渲染。
requestAnimationFrame:动画的黄金标准
什么是 requestAnimationFrame?
requestAnimationFrame(简称 rAF)是浏览器专门为动画和连续视觉更新提供的 API。它的核心特点是:在浏览器下一次重绘之前调用指定的回调函数,确保动画与屏幕刷新同步。
rAF 的基本用法
function animate() {
// 更新动画状态
updateAnimation();
// 渲染当前帧
renderFrame();
// 请求下一帧
requestAnimationFrame(animate);
}
// 启动动画循环
requestAnimationFrame(animate);
rAF 的优势
- 自动匹配显示器刷新率(通常是 60Hz)
- 页面不可见时自动暂停,节省资源
- 浏览器可以优化动画性能
- 提供精确的时间戳参数
rAF 的工作原理
function experimentRAF() {
console.log('实验开始');
let frameCount = 0;
let lastTimestamp = 0;
function frameCallback(timestamp) {
frameCount++;
if (lastTimestamp > 0) {
const interval = timestamp - lastTimestamp;
console.log(`第${frameCount}帧,间隔:${interval.toFixed(2)}ms`);
}
lastTimestamp = timestamp;
if (frameCount < 10) {
requestAnimationFrame(frameCallback);
} else {
console.log('实验结束,平均帧率:', (1000 / ((timestamp - startTime) / 10)).toFixed(1), 'fps');
}
}
const startTime = performance.now();
requestAnimationFrame(frameCallback);
}
这里的关键点在于 frameCallback() 回调中的 timestamp 参数。这个时间值是 performance.now() 返回的高精度时间,表示回调开始执行的时刻,非常适合用来计算帧间隔。
rAF 在事件循环中的位置
理解 rAF 的执行时机对于调试至关重要。我们可以通过以下代码观察它在事件循环中的确切位置:
setTimeout(() => {
console.log('1. setTimeout - 宏任务');
Promise.resolve().then(() => {
console.log('2. setTimeout 中的微任务');
});
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise - 微任务');
requestAnimationFrame(() => {
console.log('4. Promise 中注册的 rAF');
});
});
requestAnimationFrame(() => {
console.log('5. 直接注册的 rAF');
setTimeout(() => {
console.log('6. rAF 中注册的 setTimeout');
}, 0);
});
queueMicrotask(() => {
console.log('7. queueMicrotask - 微任务');
});
console.log('8. 同步代码');
输出顺序如下:
- 8. 同步代码
-
- Promise - 微任务
-
- queueMicrotask - 微任务
-
- setTimeout - 宏任务
-
- setTimeout 中的微任务
-
- 直接注册的 rAF
-
- Promise 中注册的 rAF
-
- rAF 中注册的 setTimeout
其执行过程大致为:执行宏任务 -> 执行微任务 -> 执行 rAF 回调 -> 样式计算和布局 -> 绘制 -> 合成 -> 检查空闲。
Web Workers:真正的多线程编程
什么是 Web Workers?
Web Workers 允许 JavaScript 在后台线程中运行脚本,而不会阻塞主线程。这意味着我们可以执行 CPU 密集型任务,而不会影响页面的响应性。
// 主线程代码
console.log('主线程:开始');
const worker = new Worker('worker.js');
// 向 Worker 发送消息
worker.postMessage({ type: 'CALCULATE', data: { numbers: [1, 2, 3, 4, 5] } });
// 接收 Worker 的消息
worker.onmessage = (event) => {
const result = event.data;
console.log('主线程:收到 Worker 结果', result);
document.getElementById('result').textContent = `结果:${result}`;
};
// 处理 Worker 错误
worker.onerror = (error) => {
console.error('Worker 错误:', error);
};
console.log('主线程:继续执行其他任务...');
Worker 的限制
虽然功能强大,但 Worker 也有严格的限制:
- 无法访问 DOM
- 无法使用 window、document 等全局对象
- 不能执行同步的 XHR(可以使用 fetch)
- 有同源策略限制
- 不能加载本地文件(file://协议)
Web Workers 的类型
1. 专用 Worker (Dedicated Worker)
只能被创建它的脚本使用:
const dedicatedWorker = new Worker('dedicated-worker.js');
2. 共享 Worker (Shared Worker)
可以被多个脚本共享(同源):
if (window.SharedWorker) {
const sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.onmessage = (event) => {
console.log('收到共享 Worker 消息:', event.data);
};
sharedWorker.port.postMessage('Hello Shared Worker');
} else {
console.log('浏览器不支持 Shared Worker');
}
3. Service Worker
用于离线缓存、推送通知等:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service-worker.js')
.then(registration => {
console.log('Service Worker 注册成功:', registration);
})
.catch(error => {
console.error('Service Worker 注册失败:', error);
});
}
4. Audio Worklet (Chrome 66+)
用于高性能音频处理:
if (window.audioContext && window.audioContext.audioWorklet) {
audioContext.audioWorklet.addModule('audio-processor.js')
.then(() => {
console.log('Audio Worklet 加载成功');
});
}
5. Paint Worklet (CSS Houdini)
用于自定义 CSS 绘制:
if (CSS.paintWorklet) {
CSS.paintWorklet.addModule('paint-worklet.js')
.then(() => {
console.log('Paint Worklet 加载成功');
});
}
requestIdleCallback:空闲期任务调度
什么是 requestIdleCallback?
requestIdleCallback(简称 rIC)允许开发者在浏览器空闲时期调度任务。这对于执行低优先级或非紧急的工作非常有用,避免影响关键的用户交互和动画。
const idleCallbackId = requestIdleCallback((deadline) => {
console.log('空闲回调开始执行');
console.log('剩余时间:', deadline.timeRemaining(), 'ms');
console.log('是否超时:', deadline.didTimeout);
while (deadline.timeRemaining() > 0 && hasMoreWork()) {
doSomeLowPriorityWork();
}
if (hasMoreWork()) {
requestIdleCallback(processLowPriorityWork);
}
console.log('空闲回调结束');
}, { timeout: 1000 });
console.log('主线程继续执行...');
rIC 的关键特点
- 只在浏览器空闲时执行
- 提供 deadline 对象,包含剩余时间信息
- 可以设置 timeout 确保执行
- 适合低优先级、可中断的任务
rIC 在事件循环中的位置
console.log('=== 事件循环中各 API 的执行时机 ===');
setTimeout(() => {
console.log('1. setTimeout - 宏任务');
}, 0);
Promise.resolve().then(() => {
console.log('2. Promise - 微任务');
});
requestAnimationFrame(() => {
console.log('3. requestAnimationFrame - 动画帧回调');
requestIdleCallback(() => {
console.log('5. rAF 中安排的 rIC - 空闲回调');
}, { timeout: 100 });
});
requestIdleCallback(() => {
console.log('4. 直接安排的 rIC - 空闲回调');
Promise.resolve().then(() => {
console.log('6. rIC 中的 Promise - 微任务');
});
}, { timeout: 100 });
queueMicrotask(() => {
console.log('7. queueMicrotask - 微任务');
});
console.log('8. 同步代码');
输出顺序:
- 8. 同步代码
-
- Promise - 微任务
-
- queueMicrotask - 微任务
-
- setTimeout - 宏任务
-
- requestAnimationFrame - 动画帧回调
-
- 直接安排的 rIC - 空闲回调
-
- rIC 中的 Promise - 微任务
-
- rAF 中安排的 rIC - 空闲回调
执行流程总结:宏任务 -> 微任务 -> rAF 回调 -> 样式计算 -> 绘制 -> 合成 -> 检查空闲时间执行 rIC。
核心概念总结
requestAnimationFrame (rAF)
- 是什么:浏览器提供的动画 API,在每次重绘前执行回调
- 为什么用:自动匹配显示器刷新率,页面不可见时暂停,节省资源
- 最佳时机:视觉更新、动画、连续状态变化
- 执行位置:在微任务之后,重绘之前
Web Workers
- 是什么:允许 JavaScript 在后台线程运行的技术
- 为什么用:执行 CPU 密集型任务而不阻塞主线程
- 限制:无法访问 DOM,通过消息传递通信
- 类型:专用 Worker、共享 Worker、Service Worker 等
requestIdleCallback (rIC)
- 是什么:在浏览器空闲时调度任务的 API
- 为什么用:执行低优先级、非紧急任务
- 关键对象:deadline 包含剩余时间和超时信息
- 执行位置:在一帧的最后,如果有空闲时间

