requestIdleCallback best practice

Record a practice of learning requestIdleCallback api


在学习React的过程中,了解到React Fiber 架构借鉴了requestIdleCallback API实现了强大的调度机制(Scheduler)。将渲染任务拆分成小单元,在浏览器空闲期执行,同时支持任务中断与恢复,并引入任务优先级概念。为此想了解一下 requestIdleCallback(MDN), 并通过一个简单的例子进行实践。


简介

requestIdleCallback 是浏览器提供的一个 API,它允许开发者在浏览器空闲时执行低优先级的任务,而不会影响用户体验。
用一张图来理解:

javascript 复制代码
requestIdleCallback(callback, options)

callback(deadline)
deadline = {
  timeRemaining: () => number //当前浏览器剩余时间(帧剩余时间)
  didTimeout: boolean //是否超时
}

options: { timeout: number }

1.requestIdleCallback接受两个参数,
第一个是回调函数,回调函数也就是在当前帧空闲时间要执行的任务。回调函数参数是一个对象,timeRemaining() 用来获取当前值剩余时间 didTimeout 判断是否超时。
第二个参数配置项的参数有 timeout 可配置,因为有些情况下浏览器一直繁忙而无法在指定的时间内得到执行,浏览器将会强制在timeout时间到达时执行该回调。

实践

一个简单的在dom上创建 div的例子, 由于我们在循环里一下子创建了 5w个节点,你会发现页面直接卡死,过了许久之后,才展示出创建的dom。

javascript 复制代码
const onceBtn = document.getElementById("once");
onceBtn.onclick = () => {
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < 50000; i++) {
    const div = document.createElement("div");
    div.textContent = i;
    fragment.appendChild(div);
  }
  container.appendChild(fragment);
};

那么如何用requestIdleCallback这个API进行优化呢?首先我们可以尝试实现一下伪代码:

javascript 复制代码
//伪代码
const performTask = () => {
  const _run2 = () => {
    requestIdleCallback(() => {
      while (当前还有任务要执行 && 这一帧还有空余时间) {
        执行任务;
      }
      if (当前还有任务要执行) {
        _run2();
      }
    });
  };
  _run2();
};

很好理解,只要有任务存在就递归的调用requestIdleCallback 来帮我们分批次的处理任务。

代码实现:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <button id="once">同时创建</button>
    <button id="batch">分批创建</button>

    <div class="container"></div>

    <script>
      const onceBtn = document.getElementById("once");
      const batchBtn = document.getElementById("batch");
      const container = document.querySelector(".container");

      onceBtn.onclick = () => {
        const fragment = document.createDocumentFragment();
        for (let i = 0; i < 50000; i++) {
          const div = document.createElement("div");
          div.textContent = i;
          fragment.appendChild(div);
        }
        container.appendChild(fragment);
      };

      let idleCallbackId = null;
      batchBtn.onclick = () => {
        const total = 50000;
        let current = 0;
        const batchSize = 100;

        if (idleCallbackId !== null) {
          cancelIdleCallback(idleCallbackId);
        }

        const _run = () => {
          idleCallbackId = requestIdleCallback((deadline) => {
            while (deadline.timeRemaining() > 0 && current < total) {
              const fragment = document.createDocumentFragment();
              const end = Math.min(current + batchSize, total);
              for (let i = current; i < end; i++) {
                const div = document.createElement("div");
                div.textContent = i;
                fragment.appendChild(div);
              }
              container.appendChild(fragment);
              current = end;
            }
            if (current < total) {
              _run();
            }
          });
        };
        _run();
      };
    </script>
  </body>
</html>

由于在空闲时间执行并且将任务分批次执行,我们可以看到页面上直接创建出了div。由于大量的dom创建,页面还是会卡顿的。这个例子只是用来参考。

最终效果

其实这个例子是不太合适的,requestIdleCallback 在性能优化方面非常有用,但也需要适合的场景比如:

  • ✅ 低优先级的后台任务
  • ✅ 可以分批执行的工作
  • ✅ 不紧急的 UI 更新
    希望大家在未来项目性能优化方面可以想到它。