requestIdleCallback
requestIdleCallback
是一个浏览器API,允许开发者在浏览器空闲时期执行后台或低优先级的任务,而不会影响关键的用户交互和动画性能。
1. 基本概念
工作原理
// 基本语法
const handle = requestIdleCallback(callback[, options])// 取消回调
cancelIdleCallback(handle)
基本使用
// 简单的空闲回调
requestIdleCallback(function(deadline) {// deadline 对象包含:// - didTimeout: boolean - 回调是否因超时而被执行// - timeRemaining(): function - 返回当前帧剩余的时间(毫秒)while (deadline.timeRemaining() > 0) {// 执行一些低优先级任务performLowPriorityWork();}// 如果还有未完成的工作,可以再次请求if (hasMoreWork) {requestIdleCallback(arguments.callee);}
});
2. 详细用法
deadline 对象详解
requestIdleCallback(function(deadline) {console.log('是否超时:', deadline.didTimeout);console.log('剩余时间:', deadline.timeRemaining(), 'ms');// timeRemaining() 最大返回50ms,确保不阻塞用户交互const timeRemaining = deadline.timeRemaining();if (timeRemaining > 0) {// 有时间可以执行任务processBatchOfItems();}
});
带选项的调用
// 设置超时时间(毫秒)
requestIdleCallback(function(deadline) {if (deadline.didTimeout) {// 因超时触发,需要尽快完成工作processWorkQuickly();} else {// 正常空闲时期,可以按部就班工作processWorkNormally(deadline);}},{ timeout: 2000 } // 2秒后即使不空闲也执行
);
3. 实际应用场景
场景1:数据预处理
class DataPreprocessor {constructor() {this.pendingWork = [];this.isProcessing = false;}schedulePreprocessing(dataChunks) {this.pendingWork.push(...dataChunks);if (!this.isProcessing) {this.startProcessing();}}startProcessing() {this.isProcessing = true;requestIdleCallback((deadline) => {this.processBatch(deadline);});}processBatch(deadline) {while (this.pendingWork.length > 0 && deadline.timeRemaining() > 0) {const chunk = this.pendingWork.shift();this.preprocessChunk(chunk);}if (this.pendingWork.length > 0) {// 还有工作,继续调度requestIdleCallback((deadline) => {this.processBatch(deadline);});} else {this.isProcessing = false;}}preprocessChunk(chunk) {// 模拟数据预处理console.log('处理数据块:', chunk);// 实际可能是:数据清洗、格式转换、计算等}
}// 使用示例
const preprocessor = new DataPreprocessor();
preprocessor.schedulePreprocessing([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
场景2:日志批量发送
class LazyLogger {constructor() {this.logQueue = [];this.isScheduled = false;}log(message) {this.logQueue.push({message,timestamp: Date.now()});this.scheduleFlush();}scheduleFlush() {if (this.isScheduled || this.logQueue.length === 0) {return;}this.isScheduled = true;requestIdleCallback((deadline) => {this.flushLogs(deadline);},{ timeout: 5000 } // 最多等待5秒);}flushLogs(deadline) {const batch = [];while (this.logQueue.length > 0 && (deadline.timeRemaining() > 0 || deadline.didTimeout)) {batch.push(this.logQueue.shift());// 限制单次批量大小if (batch.length >= 50) break;}if (batch.length > 0) {this.sendToServer(batch);}if (this.logQueue.length > 0) {this.scheduleFlush();} else {this.isScheduled = false;}}sendToServer(logs) {// 模拟发送到服务器console.log('发送日志到服务器:', logs);// fetch('/api/logs', { method: 'POST', body: JSON.stringify(logs) })}
}// 使用示例
const logger = new LazyLogger();
logger.log('用户登录');
logger.log('页面浏览');
logger.log('按钮点击');
场景3:DOM更新优化
class LazyDOMUpdater {constructor(container) {this.container = container;this.pendingUpdates = [];this.isUpdating = false;}scheduleUpdate(element, newContent) {this.pendingUpdates.push({ element, newContent });if (!this.isUpdating) {this.startUpdates();}}startUpdates() {this.isUpdating = true;requestIdleCallback((deadline) => {this.processUpdates(deadline);});}processUpdates(deadline) {const fragment = document.createDocumentFragment();let updatesProcessed = 0;while (this.pendingUpdates.length > 0 && deadline.timeRemaining() > 0) {const update = this.pendingUpdates.shift();this.applyUpdate(fragment, update);updatesProcessed++;// 限制单批次更新数量if (updatesProcessed >= 10) break;}// 一次性更新DOMthis.container.appendChild(fragment);if (this.pendingUpdates.length > 0) {requestIdleCallback((deadline) => {this.processUpdates(deadline);});} else {this.isUpdating = false;}}applyUpdate(fragment, update) {const div = document.createElement('div');div.textContent = update.newContent;fragment.appendChild(div);}
}// 使用示例
const container = document.getElementById('content');
const updater = new LazyDOMUpdater(container);// 批量调度更新
for (let i = 0; i < 100; i++) {updater.scheduleUpdate(null, `内容项 ${i}`);
}
4. 高级用法和最佳实践
结合 Promise 使用
function idlePromise(timeout) {return new Promise((resolve) => {requestIdleCallback((deadline) => {resolve(deadline);},timeout ? { timeout } : undefined);});
}// 使用 async/await
async function processHeavyWork() {const deadline = await idlePromise(1000);if (deadline.timeRemaining() > 0 || deadline.didTimeout) {// 执行繁重任务await heavyCalculation();}
}
性能监控
function monitoredIdleCallback(callback, options) {const startTime = performance.now();return requestIdleCallback((deadline) => {const callbackStart = performance.now();const queueTime = callbackStart - startTime;callback(deadline);const executionTime = performance.now() - callbackStart;// 监控指标console.log(`队列等待时间: ${queueTime}ms`);console.log(`执行时间: ${executionTime}ms`);console.log(`剩余时间: ${deadline.timeRemaining()}ms`);// 可以发送到监控系统if (executionTime > 50) {console.warn('空闲回调执行时间过长');}}, options);
}
错误处理
function safeIdleCallback(callback, options) {return requestIdleCallback((deadline) => {try {callback(deadline);} catch (error) {console.error('空闲回调执行出错:', error);// 错误上报reportError(error);}}, options);
}// 使用安全版本
safeIdleCallback((deadline) => {// 可能出错的代码riskyOperation();
});
5. 兼容性和回退方案
// 兼容性检查
const supportsIdleCallback = 'requestIdleCallback' in window;// 回退到 setTimeout
const idleCallback = supportsIdleCallback ? requestIdleCallback : (callback) => setTimeout(() => callback({timeRemaining: () => 50, // 模拟50msdidTimeout: false}), 1);const cancelIdleCallback = supportsIdleCallback ?cancelIdleCallback :clearTimeout;// 使用兼容版本
const handle = idleCallback((deadline) => {// 你的代码
});// 需要时取消
// cancelIdleCallback(handle);
6. 注意事项
- 不要用于关键任务:空闲回调的执行时间不确定
- 避免修改DOM:可能引起重排重绘,影响性能
- 任务要可中断:确保能在timeRemaining()用尽时暂停
- 内存管理:及时取消不再需要的回调
- 超时设置谨慎:超时可能影响用户体验
// 不好的做法 - 可能阻塞用户交互
requestIdleCallback(() => {// 执行同步的繁重计算heavySynchronousCalculation(); // ❌
});// 好的做法 - 任务可分割
requestIdleCallback(function processChunk(deadline) {while (deadline.timeRemaining() > 0 && hasMoreWork) {processWorkUnit();}if (hasMoreWork) {requestIdleCallback(processChunk);}
});
requestIdleCallback
是优化网页性能的重要工具,特别适合处理那些重要但不紧急的后台任务。