requestIdleCallback 是一个 Web API,它允许开发者在浏览器空闲时执行某些任务。这个 API 设计目的是为了让开发者能够在浏览器渲染帧之间,或其他非关键时刻执行一些低优先级的工作,从而避免阻塞主线程,影响用户的交互体验。
1. 来源和背景
requestIdleCallback 是由 Chrome 首先引入的,并且已经被其他浏览器(如 Firefox、Safari)部分支持。它是在 Web Performance API 的一部分,并且专门为 “空闲时间” 提供的 API。
空闲时间通常是指,浏览器没有被用户交互(如点击、滚动等)或更新渲染(如页面动画、DOM更新等)占用的时间。
与 setTimeout 和 setInterval 等传统的定时器 API 不同,浏览器将 requestIdleCallback 调度为一个任务队列中的回调函数,且该回调函数将在浏览器空闲时执行,这样可以最大化渲染的流畅度,避免占用主线程资源。
1.1 浏览器的事件循环
在了解 requestIdleCallback 之前,先理解浏览器的事件循环机制。
事件循环是浏览器处理 JavaScript 代码、用户输入、渲染和其他任务的核心机制。它的基本流程如下:
- 执行栈:当 JavaScript 代码被执行时,它会被推入执行栈。执行栈是一个后进先出(LIFO)的数据结构。
- 任务队列:当异步操作(如网络请求、定时器等)完成时,相关的回调会被放入任务队列。任务队列是一个先进先出(FIFO)的数据结构。
- 渲染队列:浏览器会在适当的时候进行页面渲染。渲染通常发生在 JavaScript 执行完成后。
- 空闲时间:当执行栈为空且没有待处理的任务时,浏览器会进入空闲状态。
1.2 空闲时间计算
1. 检测空闲状态
- 执行栈为空:浏览器会检查执行栈是否为空。如果没有正在执行的 JavaScript 代码,浏览器认为可以进入空闲状态。
- 任务队列为空:浏览器还会检查任务队列是否为空。如果没有待处理的任务,浏览器就会认为当前处于空闲状态。
2. 计算空闲时间
- 时间片:浏览器通常为每个帧分配一个时间片,通常是 16 毫秒(对应 60 FPS 的帧率)。在这个时间片内,如果没有任务需要执行,浏览器就会认为有空闲时间。
- 剩余时间:当 requestIdleCallback 的回调被调用时,开发者可以使用 deadline.timeRemaining() 方法来获取当前空闲时间的剩余毫秒数。这个方法返回一个数字,表示在下一个帧渲染之前的剩余时间。
下面先简单提一下具体流程。
3. 处理流程
当调用 requestIdleCallback 时,流程如下:
- 调用 requestIdleCallback:传入一个回调函数,浏览器将其排入待执行的队列。
-
requestIdleCallback(myCallback);
-
- 空闲检查
- 浏览器在每一帧中检查执行栈和任务队列的状态。如果执行栈为空且任务队列也为空,浏览器会认为当前处于空闲状态。
- 执行回调
- 如果当前没有任务需要处理,浏览器会调用 requestIdleCallback 中的回调函数。
- 在回调中,使用 deadline.timeRemaining() 来判断当前的空闲时间。
-
function myCallback(deadline) { while (deadline.timeRemaining() > 0) { // 执行一些非紧急的任务 console.log('执行任务...'); } // 如果还有任务未完成,可以在下次空闲时继续处理 if (/* 还有更多任务 */) { requestIdleCallback(myCallback); } }
- 超时处理
- 如果设置了超时参数,浏览器会在超时到达时强制调用回调,即使没有空闲时间。
-
requestIdleCallback(myCallback, { timeout: 1000 });
2. 基本用法
requestIdleCallback 用法与传统的 setTimeout 类似,但它有一些特殊的处理机制。最简单的使用方式如下:
<body>
<button id="startButton">启动空闲任务</button>
<div id="output"></div>
<script>
const startButton = document.getElementById('startButton');
const output = document.getElementById('output');
// 定义一个模拟的低优先级任务
function lowPriorityTask(deadline) {
// deadline.timeRemaining() 返回当前空闲时间还剩下多少毫秒
while (deadline.timeRemaining() > 0) {
// 模拟一些耗时操作
const newElement = document.createElement('p');
newElement.textContent = `随机数生成: ${Math.random()}`;
output.appendChild(newElement);
// 如果已经生成了 100 个随机数,停止任务
if (output.children.length >= 100) {
return;
}
}
// 如果空闲时间不足,继续请求下一个空闲回调
requestIdleCallback(lowPriorityTask);
}
// 给按钮添加点击事件监听器
startButton.addEventListener('click', () => {
// 开始请求空闲回调
requestIdleCallback(lowPriorityTask);
});
</script>
</body>
参数说明:
- deadline:该对象包含有关剩余空闲时间的信息,主要有两个属性:
- timeRemaining():返回当前空闲周期中剩余的时间,单位是毫秒。
- didTimeout:布尔值,指示回调是否因为超时而被执行。
Tip:requestIdleCallback 不会强制执行任务,它会尽量在浏览器空闲时执行回调,但如果浏览器忙于其他任务,回调可能会延迟执行。
3. 引入原因
浏览器的渲染引擎通常会占用主线程来处理用户交互和渲染工作,这导致开发者有时难以在这些繁忙时刻执行一些额外的任务(例如后台数据加载、日志记录、非关键的 DOM 操作等)。
requestIdleCallback 主要解决问题包括:
- 避免阻塞主线程:通过让低优先级任务在空闲时执行,避免影响页面渲染或用户交互。
- 提高性能:允许开发者在用户不操作时做一些后台处理,避免强制中断用户体验。
3.1 优缺点
1、优点
- 非阻塞性:任务会在浏览器空闲时执行,因此不会中断用户的交互和页面渲染。
- 灵活性:通过 timeRemaining() 函数,开发者可以判断当前空闲时间是否足够,灵活安排任务。
- 性能优化:有助于处理一些低优先级的任务(如懒加载、数据同步、日志记录等),从而避免主线程长时间被占用,提高用户体验。
2、缺点
- 浏览器支持有限:虽然在 Chrome 中已被支持,但其他浏览器(如 Safari 和 Firefox)并不全面支持。如果存在兼容多个浏览器问题,就需要额外处理。
- 无法控制任务的精确执行时机:回调函数的执行是由浏览器调度的,不能精确控制。任务可能会延迟执行,甚至在极端情况下根本不执行(例如,如果页面一直繁忙)。
- 不保证立即执行:与 setTimeout 和 setInterval 不同,requestIdleCallback 并不保证立即执行回调函数。它是根据空闲时间来安排执行的。
4. 对比:requestIdleCallback vs setTimeout vs setInterval
特性 | requestIdleCallback | setTimeout | setInterval |
---|---|---|---|
执行时机 | 在浏览器空闲时执行(优先级低的任务) | 在指定的时间后执行,可能会在高优先级任务期间被中断 | 在指定时间间隔周期性执行,可能会在高优先级任务期间被中断 |
适用场景 | 空闲时间的低优先级任务(如懒加载、后台同步、日志记录等) | 延迟执行某个任务,通常用于定时操作(如动画、轮询) | 定时周期性执行任务(如轮询、计时器) |
优先级 | 低优先级任务,且不会阻塞页面渲染或用户交互 | 可以与浏览器渲染任务争夺主线程资源,影响性能 | 与 setTimeout 类似,可能影响页面渲染性能 |
支持性 | 目前主要在 Chrome 支持,其他浏览器支持较差 | 广泛支持(几乎所有浏览器) | 广泛支持(几乎所有浏览器) |
可控性 | 不能精确控制执行时机,只能在空闲时执行 | 可以精确控制执行时机 | 可以精确控制执行时机,周期性执行任务 |
5. 实际 🌰
1. 延迟加载图片
典型例子:假设有一个网页,里面有很多图片,希望用户滚动到这些图片时再加载它们。可以使用 requestIdleCallback 来在浏览器空闲时加载这些图片。
const images = document.querySelectorAll('img[data-src]');
function loadImages() {
images.forEach((img) => {
if (img.getBoundingClientRect().top < window.innerHeight) {
img.src = img.dataset.src; // 将 data-src 的值赋给 src
img.removeAttribute('data-src'); // 移除 data-src 属性
}
});
}
function idleCallback(deadline) {
while (deadline.timeRemaining() > 0 && images.length > 0) {
loadImages();
}
}
// 每次空闲时调用
requestIdleCallback(idleCallback);
2. 分块处理任务
假设有一个需要处理大量数据的任务,比如渲染一个长列表。可以将这个任务分成多个小块,在浏览器空闲时逐步处理。
const data = Array.from({ length: 10000 }, (_, i) => `Item ${i + 1}`);
let index = 0;
function renderItems(deadline) {
while (deadline.timeRemaining() > 0 && index < data.length) {
const item = document.createElement('div');
item.textContent = data[index++];
document.body.appendChild(item);
}
if (index < data.length) {
requestIdleCallback(renderItems); // 继续处理剩余的项
}
}
// 开始处理
requestIdleCallback(renderItems);