requestIdleCallback 核心作用
requestIdleCallback
是浏览器提供的 API,用于将非关键任务延迟到浏览器空闲时段执行,避免阻塞用户交互、动画等关键任务,从而提升页面性能体验。
基本语法
const handle = window.requestIdleCallback(callback[, options])
参数
callback:一个将在浏览器空闲时期被调用的函数。该回调函数接收一个参数:
IdleDeadline
对象,包含:timeRemaining()
:返回当前帧剩余的空闲时间(毫秒),通常 ≤ 50msdidTimeout
:布尔值,表示是否因为指定的 timeout 时间已到而触发回调
options(可选):配置对象
timeout
:如果指定了 timeout,并且回调在 timeout 毫秒后还没有被调用,则回调会在下一次有机会时被强制执行
返回值
返回一个 ID,可以传递给 cancelIdleCallback()
来取消回调。
配套方法
window.cancelIdleCallback(handle)
取消之前通过 requestIdleCallback()
安排的回调。
工作原理
浏览器在每一帧渲染完成后会检查是否有空闲时间
如果有空闲时间,且存在待执行的 idle 回调,则执行它们
每次 idle 回调执行时,可以通过
timeRemaining()
检查剩余时间如果任务未完成,可以在回调中再次调用
requestIdleCallback
继续处理
使用示例
基本用法
function processInIdleTime(deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
performTask(tasks.pop());
}
if (tasks.length > 0) {
requestIdleCallback(processInIdleTime);
}
}
requestIdleCallback(processInIdleTime);
带超时的用法
requestIdleCallback(processInIdleTime, { timeout: 2000 });
// 保证在2秒内执行,即使浏览器一直不空闲
关键特性
特性 | 说明 |
---|---|
空闲期执行 | 只在浏览器主线程空闲时运行(每帧渲染后的空闲时间) |
可中断性 | 如果用户开始交互,任务会被暂停 |
超时控制 | 可通过 timeout 参数强制在指定时间后执行(避免长期等待) |
适用场景
日志上报和分析:将非关键的日志发送推迟到空闲时间
预加载资源:预加载接下来可能需要的非关键资源
大数据处理:分块处理大型数据集,避免界面卡顿
非关键UI更新:如更新界面上的辅助信息或统计数字
注意事项
不要用于关键任务:空闲回调可能永远不会执行,或者执行得很晚
任务应该可分片:每次回调应该只处理一小部分工作
避免DOM操作:在空闲回调中进行DOM操作可能触发重排/重绘
超时设置要合理:过短的 timeout 会使 API 失去意义,过长则影响体验
浏览器兼容性分析
✅ 完全支持的浏览器
Chrome
版本:47+(2015年发布)
备注:包括所有基于 Chromium 的浏览器(Edge、Opera 等)
Firefox
版本:55+(2017年发布)
备注:在移动端和桌面端表现一致
Edge
版本:79+(Chromium 内核版本)
⚠️ 部分支持/行为差异的浏览器
Safari
版本:部分支持(需检测)
问题:
iOS Safari 和 macOS Safari 实现可能不一致
某些版本中
timeRemaining()
返回值不准确
❌ 不支持的浏览器
Internet Explorer
所有版本均不支持
旧版 Edge(EdgeHTML 内核)
版本:18 及以下
Android 默认浏览器(4.4及以下)
兼容性风险点列表
移动端注意
部分安卓 WebView(特别是 Hybrid 应用内嵌浏览器)可能不支持
Safari 特殊性
某些版本即使支持 API,空闲时间计算可能不准确
隐身模式影响
部分浏览器在隐身模式下会限制后台任务执行
兼容性解决方案列表
特性检测标准写法
const hasIdleCallback = 'requestIdleCallback' in window;
推荐降级方案
优先降级到 requestAnimationFrame(适合视觉相关任务)
其次降级到 setTimeout(callback, 0)(通用方案)
Polyfill 选择
官方推荐的 polyfill
注意:polyfill 无法真正模拟空闲期,只是延迟执行
/**
* 增强型空闲任务调度器(支持多级降级方案)
* @param {Function} callback - 需要执行的回调函数,接收 deadline 对象
* @param {Object} [options] - 配置选项
* @param {number} [options.timeout=0] - 超时时间(毫秒)
* @returns {number} 调度器ID(可用于取消)
*/
function enhancedRequestIdleCallback(callback, options = {}) {
// 参数有效性检查
if (typeof callback !== 'function') {
throw new TypeError('回调必须是函数');
}
const { timeout = 0 } = options;
// 原生支持检测
if ('requestIdleCallback' in window) {
return window.requestIdleCallback(callback, { timeout });
}
// ========== 降级方案实现 ==========
let id;
const start = Date.now();
const isVisualTask = isRelatedToVisualUpdate(callback);
// 方案1:视觉相关任务使用 requestAnimationFrame
if (isVisualTask && 'requestAnimationFrame' in window) {
id = window.requestAnimationFrame(() => {
callback({
timeRemaining: () => Math.max(0, 16.6 - (Date.now() - start)),
didTimeout: Date.now() - start >= timeout
});
});
}
// 方案2:通用任务使用 setTimeout
else {
// 计算合理延迟时间(避免过度消耗资源)
const delay = calculateSafeDelay(isVisualTask);
id = window.setTimeout(() => {
callback({
timeRemaining: () => 1, // 模拟1ms剩余时间
didTimeout: true // 降级模式下总是触发超时
});
}, delay);
}
// 添加超时强制触发机制
if (timeout > 0) {
const timeoutId = setTimeout(() => {
callback({
timeRemaining: () => 0,
didTimeout: true
});
clearTimeout(id);
}, timeout);
// 返回复合ID用于取消
return { rId: id, tId: timeoutId };
}
return id;
}
/**
* 取消空闲任务调度
* @param {number|Object} id - 调度器返回的ID
*/
function enhancedCancelIdleCallback(id) {
if ('cancelIdleCallback' in window) {
window.cancelIdleCallback(id);
return;
}
// 处理复合ID(超时场景)
if (typeof id === 'object') {
clearTimeout(id.tId);
id = id.rId;
}
// 根据降级方案取消
if ('cancelAnimationFrame' in window) {
window.cancelAnimationFrame(id);
} else {
clearTimeout(id);
}
}
// ========== 工具函数 ==========
/**
* 判断任务是否与视觉更新相关
* (根据常见DOM API使用模式推测)
*/
function isRelatedToVisualUpdate(fn) {
const fnStr = fn.toString();
return /(offset|scroll|client|getBounding|style)/.test(fnStr);
}
/**
* 计算安全延迟时间
* 视觉任务:下一帧时间(16.6ms)
* 非视觉任务:分级延迟(0-50ms随机)
*/
function calculateSafeDelay(isVisual) {
return isVisual ? 16 : Math.min(50, Math.floor(Math.random() * 50));
}
// ========== 使用示例 ==========
// 示例任务
function backgroundTask(deadline) {
while (deadline.timeRemaining() > 0) {
// 执行任务分片...
}
if (hasMoreWork) {
enhancedRequestIdleCallback(backgroundTask);
}
}
// 启动任务
const taskId = enhancedRequestIdleCallback(backgroundTask, { timeout: 2000 });
// 取消任务
// enhancedCancelIdleCallback(taskId);
注意事项
避免在回调中修改 DOM(可能触发重排)
空闲时间不保证,任务应有中断/恢复机制
耗时任务应使用 Web Worker
requestIdleCallback
的详细介绍和示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>requestIdleCallback 示例与兼容性处理</title>
</head>
<body>
<h1>requestIdleCallback 演示</h1>
<div id="output"></div>
<script>
/**
* 兼容性处理:如果原生不支持 requestIdleCallback,
* 使用 setTimeout 实现降级方案
*/
window.requestIdleCallback = window.requestIdleCallback || function(cb) {
// 降级方案:用 50ms 延迟模拟空闲时段
let start = Date.now();
return setTimeout(function() {
cb({
didTimeout: false,
timeRemaining: function() {
// 确保至少留出 1ms 时间
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
};
/**
* 分块任务处理器
* @param {Array} taskList - 要处理的任务数组
* @param {Function} processor - 单个任务处理函数
* @param {number} chunkSize - 每次处理的任务数(默认 10)
*/
function processTasksInIdle(taskList, processor, chunkSize = 10) {
let index = 0;
function doChunk(deadline) {
// 当剩余时间 > 0 或超时前处理任务
while (
(deadline.timeRemaining() > 0 || deadline.didTimeout) &&
index < taskList.length
) {
// 每次处理指定数量的任务
const tasksToProcess = taskList.slice(index, index + chunkSize);
tasksToProcess.forEach(task => processor(task));
index += chunkSize;
// 更新页面显示进度
updateProgress(index);
}
// 如果还有剩余任务,继续调度
if (index < taskList.length) {
// 使用超时参数 100ms 保证即使不空闲也会执行
requestIdleCallback(doChunk, { timeout: 100 });
}
}
// 初始调用
requestIdleCallback(doChunk, { timeout: 100 });
}
// 示例:创建 500 个元素的列表(模拟大量任务)
const dummyTasks = new Array(500).fill(null).map((_, i) => ({
id: i + 1,
content: `Item ${i + 1}`
}));
// 任务处理函数(模拟DOM操作)
function handleTask(task) {
const div = document.createElement('div');
div.textContent = task.content;
// 这里可以添加更复杂的操作
}
// 更新进度显示
function updateProgress(processedCount) {
const output = document.getElementById('output');
output.textContent = `已处理 ${processedCount}/${dummyTasks.length} 项任务`;
}
// 启动任务处理(页面加载完成后)
window.addEventListener('load', () => {
processTasksInIdle(dummyTasks, handleTask, 10); // 每次处理10个
});
</script>
<!-- 兼容性提示 -->
<script>
// 检测是否原生支持
if (!window.requestIdleCallback) {
const warn = document.createElement('p');
warn.style.color = 'red';
warn.textContent = '当前浏览器不支持 requestIdleCallback,已使用 setTimeout 降级方案';
document.body.appendChild(warn);
}
</script>
</body>
</html>