还在用 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 的浏览器中,可以保证代码结构一致、功能不报错

相关推荐
非凡ghost3 小时前
Flameshot(开源免费的截图工具) 中文绿色版
前端·javascript·后端
神秘的猪头3 小时前
Stylus项目实战:cards
前端·javascript
神秘的猪头3 小时前
在浏览器中用 JavaScript 实现自然语言处理与机器学习:从 Brain.js 到大模型时代
javascript
MiyueFE3 小时前
使用Powertools for Amazon Lambda简化Amazon AppSync Events集成
前端·aws
神秘的猪头3 小时前
弹性布局vsinline-block
前端
王六岁3 小时前
# 🐍 前端开发 0 基础学Python小结 Python数据类型使用场景与用途指南
前端·python
平生不晚3 小时前
优化使用img标签加载svg大图导致的内存开销
前端·浏览器
Zyx20073 小时前
弹性布局:告别“挤来挤去”的CSS布局时代——深入理解 Flexbox
前端·css
Apifox3 小时前
Apifox 10 月更新|支持实时预览在线文档个性化配置的效果、性能优化、测试能力升级
前端·后端·测试