还在用 setTimeout?试试 requestIdleCallback 吧!

大家好,我是 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 的浏览器中,可以保证代码结构一致、功能不报错

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax