概述
requestIdleCallback 是一个浏览器 API,允许在浏览器空闲时执行非关键任务,不会阻塞主线程。这对于提升用户体验和性能非常重要,特别是在处理大量 DOM 更新时。
实现
1. 工具函数 (idleCallback.ts)
创建一个完整的 requestIdleCallback 工具库,包括:
- requestIdleCallback: 在浏览器空闲时执行回调
- cancelIdleCallback: 取消之前调度的回调
- IdleTaskQueue: 批量处理任务的队列类
特性
- ✅ 自动降级: 对于不支持 requestIdleCallback 的浏览器,自动降级为 setTimeout + MessageChannel
- ✅ 超时保护: 支持超时配置,确保任务最终会被执行
- ✅ 批量处理: IdleTaskQueue 可以在空闲时间内批量处理多个任务
typescript
/**
* requestIdleCallback 工具函数
* 在浏览器空闲时执行回调,不会阻塞主线程
* 提供降级方案以支持不支持 requestIdleCallback 的浏览器
*/
interface IdleCallbackOptions {
timeout?: number; // 超时时间(毫秒),如果指定,回调会在超时后强制执行
}
interface IdleDeadline {
didTimeout: boolean; // 是否因为超时而执行
timeRemaining(): number; // 返回当前空闲时间(毫秒)
}
type IdleCallbackHandle = number;
/**
* 在浏览器空闲时执行回调
* @param callback 要执行的回调函数
* @param options 配置选项
* @returns 请求 ID,可用于取消
*/
export const requestIdleCallback = (
callback: (deadline: IdleDeadline) => void,
options?: IdleCallbackOptions
): IdleCallbackHandle => {
// 如果浏览器支持原生 requestIdleCallback,直接使用
if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
return window.requestIdleCallback(callback, options);
}
// 降级方案:使用 setTimeout 模拟
// 使用 1ms 延迟,让浏览器有机会处理其他任务
const timeout = options?.timeout ?? 5000; // 默认 5 秒超时
const startTime = Date.now();
const timeoutId = setTimeout(() => {
callback({
didTimeout: true,
timeRemaining: () => Math.max(0, 50 - (Date.now() - startTime)),
});
}, timeout);
// 使用 MessageChannel 实现更接近 requestIdleCallback 的行为
// MessageChannel 会在当前任务完成后、下一个任务之前执行
const channel = new MessageChannel();
channel.port1.onmessage = () => {
clearTimeout(timeoutId);
callback({
didTimeout: false,
timeRemaining: () => Math.max(0, 50 - (Date.now() - startTime)),
});
};
channel.port2.postMessage(null);
// 返回一个可以用于取消的 ID
return timeoutId as unknown as IdleCallbackHandle;
};
/**
* 取消之前通过 requestIdleCallback 调度的回调
* @param handle 之前返回的请求 ID
*/
export const cancelIdleCallback = (handle: IdleCallbackHandle): void => {
if (typeof window !== 'undefined' && 'cancelIdleCallback' in window) {
window.cancelIdleCallback(handle);
} else {
// 降级方案:清除 setTimeout
clearTimeout(handle as unknown as number);
}
};
/**
* 批量执行任务,在空闲时逐个处理
* 适用于需要处理大量非关键任务的场景
*/
export class IdleTaskQueue {
private tasks: Array<() => void> = [];
private isProcessing = false;
private currentHandle: IdleCallbackHandle | null = null;
/**
* 添加任务到队列
*/
add(task: () => void): void {
this.tasks.push(task);
this.process();
}
/**
* 批量添加任务
*/
addBatch(tasks: Array<() => void>): void {
this.tasks.push(...tasks);
this.process();
}
/**
* 处理队列中的任务
*/
private process(): void {
if (this.isProcessing || this.tasks.length === 0) {
return;
}
this.isProcessing = true;
this.currentHandle = requestIdleCallback(
(deadline) => {
// 在空闲时间内尽可能多地处理任务
while (deadline.timeRemaining() > 0 && this.tasks.length > 0) {
const task = this.tasks.shift();
if (task) {
try {
task();
} catch (error) {
console.error('IdleTaskQueue task error:', error);
}
}
}
// 如果还有任务未处理,继续调度
if (this.tasks.length > 0) {
this.isProcessing = false;
this.process();
} else {
this.isProcessing = false;
}
},
{ timeout: 5000 } // 5 秒超时,确保任务最终会被执行
);
}
/**
* 清空队列
*/
clear(): void {
this.tasks = [];
if (this.currentHandle !== null) {
cancelIdleCallback(this.currentHandle);
this.currentHandle = null;
}
this.isProcessing = false;
}
/**
* 获取队列中剩余任务数
*/
get length(): number {
return this.tasks.length;
}
}
2. DOM 更新优化
优化前:
typescript
// 使用 setTimeout 延迟更新
this.timmer = setTimeout(() => {
patch(this.vNode, vNode);
// ...
}, DOM_UPDATE_DELAY_MS);
优化后:
typescript
// 使用 requestIdleCallback 在浏览器空闲时更新
this.idleCallbackHandle = requestIdleCallback(
(deadline) => {
// 检查是否有足够的时间
if (deadline.timeRemaining() < 5 && !deadline.didTimeout) {
// 重新调度到下一个空闲周期
this.idleCallbackHandle = requestIdleCallback(/* ... */);
return;
}
this.performDomUpdate(deadline);
},
{ timeout: DOM_UPDATE_DELAY_MS }
);
优势:
- 🚀 不阻塞主线程: DOM 更新在浏览器空闲时执行
- 🎯 智能调度: 如果当前空闲时间不够,自动延迟到下一个空闲周期
- ⏱️ 超时保护: 即使浏览器一直忙碌,也会在超时后执行
3. 批量更新优化
优化前:
typescript
// 场景:数据变化时,更新对应的dom显示
handleHeartbeat = (uploader: FileUploader): void => {
for (let i = 0; i < uploader.uploadingTaskList.length; i++) {
const taskItem = uploader.uploadingTaskList[i];
const item = this.itemsMap.get(taskItem.id);
if (item) {
item.updateDom(taskItem); // 立即执行
}
}
};
优化后:
typescript
// 批量收集更新任务,在空闲时执行
handleHeartbeat = (uploader: FileUploader): void => {
const updateTasks: Array<() => void> = [];
for (let i = 0; i < uploader.uploadingTaskList.length; i++) {
const taskItem = uploader.uploadingTaskList[i];
const item = this.itemsMap.get(taskItem.id);
if (item) {
updateTasks.push(() => {
item.updateDom(taskItem);
});
}
}
// 批量添加到空闲任务队列
if (updateTasks.length > 0) {
this.idleUpdateQueue.addBatch(updateTasks);
}
};
优势:
- 📦 批量处理: 多个 DOM 更新在同一个空闲周期内批量处理
- ⚡ 减少重排: 减少浏览器重排/重绘次数
- 🎯 优先级管理: 非关键更新不会阻塞关键操作
使用场景
适合使用 requestIdleCallback 的场景
-
✅ DOM 更新(非关键)
- 进度条更新
- 状态显示更新
- 统计数据展示
-
✅ 数据统计/分析
- 上传速度计算
- 进度统计
- 性能指标收集
-
✅ 预加载/预取
- 预加载下一个文件
- 预计算 MD5
不适合使用 requestIdleCallback 的场景
-
❌ 用户交互响应
- 点击事件处理
- 输入事件处理
- 必须立即响应的操作
-
❌ 关键路径操作
- 文件上传请求
- 错误处理
- 状态变更通知
性能收益
预期改进
- 主线程阻塞减少: DOM 更新不再阻塞主线程,提升页面响应性
- 帧率提升: 减少不必要的重排/重绘,提升动画流畅度
- CPU 使用优化: 在浏览器空闲时执行任务,更好地利用 CPU 资源
- 用户体验提升: 页面更流畅,交互更及时
实际测试建议
- Chrome DevTools Performance: 检查主线程阻塞情况
- FPS 监控: 监控帧率变化
- Lighthouse: 运行性能测试
- 真实场景测试: 测试大量文件上传时的性能
浏览器兼容性
原生支持
- ✅ Chrome 47+
- ✅ Edge 79+
- ✅ Firefox 55+
- ✅ Safari 不支持(需要降级方案)
降级方案
我们的实现自动提供了降级方案:
- 使用 MessageChannel + setTimeout 模拟 requestIdleCallback
- 确保所有浏览器都能正常工作
- 性能可能略低于原生实现,但仍然比直接执行更好
最佳实践
1. 合理设置超时时间
typescript
// 对于关键更新,设置较短的超时时间
requestIdleCallback(callback, { timeout: 100 });
// 对于非关键更新,可以设置较长的超时时间
requestIdleCallback(callback, { timeout: 5000 });
2. 检查空闲时间
typescript
requestIdleCallback((deadline) => {
// 如果时间不够,延迟执行
if (deadline.timeRemaining() < 5 && !deadline.didTimeout) {
// 重新调度
return;
}
// 执行任务
performTask();
});
3. 批量处理任务
typescript
// 使用 IdleTaskQueue 批量处理
const queue = new IdleTaskQueue();
queue.addBatch([
() => updateTask1(),
() => updateTask2(),
() => updateTask3(),
]);
4. 清理资源
typescript
// 组件销毁时清理
destroy() {
if (this.idleCallbackHandle) {
cancelIdleCallback(this.idleCallbackHandle);
}
this.idleUpdateQueue.clear();
}
注意事项
- 超时时间: 设置合理的超时时间,确保任务最终会被执行
- 任务大小: 避免在单个空闲周期内执行过大的任务
- 错误处理: 确保任务中的错误不会影响后续任务
- 内存管理: 及时清理不再需要的回调句柄
未来优化方向
- IntersectionObserver: 结合使用,只更新可见区域的任务
- Web Workers: 将计算密集型任务移到 Worker 线程
- 虚拟滚动: 对于大量任务列表,使用虚拟滚动优化
- 增量更新: 只更新变化的部分,而不是整个 DOM