大家好,我是 Sunday。
在开发中,setTimeout 咱们几乎天天都在用。
无论是页面初始化后延迟执行逻辑、动画间隔,还是接口请求防抖、埋点上报,咱们几乎都离不开它。
但是 setTimeout 有时候并不好用,比如说:
- setTimeout 的执行时间并不准确,延迟时间只是任务准备已出 EventLoop 的时间
- setTimeout 并不会判断浏览器任务是否空闲,从而当任务执行时可能会出现卡顿的情况
浏览器是单线程的,所有任务都要经过**事件循环(Event Loop)**来调度。当你调用
setTimeout(fn, 0)时,这个任务会被放进 "宏任务队列" 里,只有当主线程空出来,才会去执行。
因此,如果我们想要在 浏览器空闲时间 去执行一些大任务操作(比如:埋点上报),那么 setTimeout 并不方便。
那么,有没有一个更加聪明的 API,可以知道浏览器什么时候会空闲,从而可以 自动调用 任务呢?
它就是 requestIdleCallback
requestIdleCallback

requestIdleCallback 的核心是 浏览器级空闲调度 API ,它能让你把一些非关键任务 放到浏览器"闲"的时候去执行,从而让关键任务(如渲染、动画、交互)始终保持流畅。PS: 也就是说,浏览器在处理完一帧的渲染、动画、事件之后,如果还有空余时间,就会来执行你的任务。
这个函数接收两个参数 callback, options,并且会返回一个 ID 作为结束回调参数(通过 Window.cancelIdleCallback() 结束回调)

基础应用
比如说:咱们要做一个埋点上报的系统,希望在用户浏览页面后,上报一些埋点日志。
scss
sendAnalyticsData() // 立即上报埋点
如果代码这么写,则当前代码会在页面加载阶段就发请求,不仅占用主线程,还可能影响首屏性能。
那么如果使用 requestIdleCallback ,则可以等到浏览器"闲"下来再去上报
scss
requestIdleCallback(() => {
sendAnalyticsData()
})
这就是一个典型的"低优先级任务"场景。用 requestIdleCallback,让浏览器自动帮咱们排好优先顺序。
利用 deadline 拆解任务
此时,假设我们有一个很大的任务,比如:需要遍历十万条数据进行处理
js
const arr = Array.from({ length: 100000 }, (_, i) => i)
function task() {
while (arr.length > 0) {
helloSunday(arr.shift())
}
}
function helloSunday(i) {
console.log('hello', i)
}
task()
如果咱们直接这样写代码,那么在 企业项目 中,因为还需要处理更多的额外任务,那么就一定会导致页面严重卡顿,因为 JavaScript 是单线程的,这段任务会一直占着主线程不放。
而换成 requestIdleCallback,我们可以利用 deadline.timeRemaining() 检查当前帧的"空闲时间", 把任务拆成多次执行
- deadline:浏览器传入的对象,包含当前帧的剩余空闲时间
- deadline.timeRemaining():表示当前帧还剩多少毫秒可以安全执行任务
- deadline.didTimeout:表示任务是否超时(当设置了 timeout 时,才会有用)
html
<body>
<div>测试</div>
<button onclick="renderClick()">点击,进行大量渲染</button>
<script>
const arr = Array.from({ length: 100000 }, (_, i) => i)
function workLoop(deadline) {
// 有安全执行时间时,才会执行
while (deadline.timeRemaining() > 0 && arr.length > 0) {
helloSunday(arr.shift())
}
if (arr.length > 0) {
// 再次触发空闲回调
requestIdleCallback(workLoop)
}
}
function helloSunday(i) {
console.log('hello', i)
}
requestIdleCallback(workLoop)
// 渲染大量的 div
function renderClick() {
for (let i = 0; i < 50000; i++) {
const div = document.createElement('div')
div.textContent = `点击渲染的元素 ${i}`
document.body.appendChild(div)
}
}
// 直接渲染
renderClick()
</script>
</body>
通过以上代码,咱们就可以测试出,在一开始浏览器忙的时候,requestIdleCallback 不会执行。当浏览器空闲下来之后,才会进行处理。
咱们可以通过以下的表格,来对比下两个函数的区别
| 对比项 | setTimeout | requestIdleCallback |
|---|---|---|
| 调度方式 | 固定时间 | 主线程空闲时 |
| 精准度 | 不稳定,受任务队列影响 | 智能调度,由浏览器控制(除非设置了 timeout) |
| 性能表现 | 容易卡顿 | 平滑、不打断渲染 |
| 适合场景 | 动画延迟、节流防抖 | 预加载、日志、数据缓存、计算任务 |
requestIdleCallback vs requestAnimationFrame
说完 requestIdleCallback,很多同学可能会想:它和 requestAnimationFrame(简称 rAF)是不是差不多啊?两个名字都带 request,还都和浏览器时机有关。
其实,它们的目标是完全不同的。
requestAnimationFrame:关注 渲染帧 ,保证动画和刷新同步。他会在 下一帧绘制前 调用,用来驱动动画requestIdleCallback:关注 空闲帧 ,在主线程空闲时执行任务。他会在 浏览器空闲时 调用,用来执行非关键任务
两者的典型场景
requestAnimationFrame:动画、位移动效
js
function moveBox() {
box.style.left = box.offsetLeft + 2 + 'px'
requestAnimationFrame(moveBox)
}
requestAnimationFrame(moveBox)
这类任务要求和屏幕刷新频率保持一致(比如:60fps) ,否则就会掉帧或卡顿,所以必须放在 rAF 中执行。
例如:滚动联动、进度条、骨架屏、loading 动画等。
requestIdleCallback:后台任务、预加载
js
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
helloSunday(tasks.shift())
}
})
这类任务对时机要求不高,重点是不影响渲染。当浏览器一帧执行完、空出一点时间,它就会去做这些工作。
例如:日志上报、预取缓存、离线计算、大数据分片等。
Polyfill 与兼容性方案
目前,requestIdleCallback 并不是所有浏览器都支持,尤其是 Safari 和部分移动端 WebView。
但没关系,我们可以自己实现一个简易版本(Polyfill),通过 setTimeout 来模拟「空闲回调」的效果。
js
// 如果浏览器原生不支持 requestIdleCallback,则定义一个兼容版本
if (!window.requestIdleCallback) {
window.requestIdleCallback = function (cb) {
// 记录当前时间,用于计算剩余空闲时间
const start = Date.now()
// 使用 setTimeout 模拟空闲调度
// 在 1 毫秒后异步执行回调函数 cb
return setTimeout(() => {
// 手动构造一个 deadline 对象,模拟浏览器传入的参数
cb({
// 表示任务是否超时(这里固定为 false,因为没有 timeout 机制)
didTimeout: false,
// timeRemaining 用于返回当前帧还剩下多少"空闲时间"(毫秒)
// 假设一帧 50ms(对应 20fps),
// 当前时间 - start 表示已经消耗的时间,
// 50 - 已消耗时间 = 剩余可用时间
// 若结果为负,则取 0,避免返回负值
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start))
}
})
}, 1) // 延迟 1ms 调用,避免阻塞主线程
}
}
// 如果浏览器不支持 cancelIdleCallback,则提供对应的取消方法
if (!window.cancelIdleCallback) {
window.cancelIdleCallback = function (id) {
// 直接调用 clearTimeout 取消 setTimeout 模拟的任务
clearTimeout(id)
}
}
虽然这种方式无法真正识别主线程空闲时间 ,但在不支持 requestIdleCallback 的浏览器中,可以保证代码结构一致、功能不报错。