1. What is requestIdleCallback

In one sentence

RequestIdleCallback is an experimental API that lets you do things when your browser is idle. To begin with, you can simply copy the following code from the console:

function work(deadline) { // Deadline has a method called timeRemaining() to retrieve the remaining free time of the current browser, in ms; There's a property didTimeout that says whether or not you're going to time out
  console.log('Remaining time of current frame:${deadline.timeRemaining()}`);
  if (deadline.timeRemaining() > 1 || deadline.didTimeout) {
     // If we have enough time, we can write our own logic here
  }
  // Run out of time, give control to the main thread, the next idle call
  requestIdleCallback(work);
}
requestIdleCallback(work, { timeout: 1000 }); // You can pass a callback function (mandatory) and arguments (currently only timeout).
Copy the code

If you run it and nothing happens to the page, it will print most of the time in about 49.9ms. If you wiggle the mouse a little bit, most of it will take less than 16ms to print because the browser is not idle.

What is free time?

We know that the page is drawn frame by frame, generally 60 frames per second is smooth for us, which corresponds to about 16ms per frame, so if something is executed per frame (Task + render +…) Is less than 16ms, indicating that there is free time to use, as shown in the following figure (fromW3C😄) :There’s another idle situation where the page hasn’t worked for a long timerequestIdleCallbackThe remaining time will be as long as possible, 50ms at most, as shown in the figure below (coincidentally, also fromW3C😄) :Therefore, we can see that at the beginning of the experiment, the printing probability is 49.9ms without operation of the page, and the printing probability is less than 16ms after moving the mouse. So why 50ms? The simple understanding is that this is an empirical or statistical value, because if the idle time is given too long, it will not respond well to high-priority tasks (such as keyboard events) occurring in the interval and may feel a drop delay.

What can be done? 🤔

Do non-superior, demergable, manageable tasks. (Yes and no, so let’s look at some specific examples.)

Data analysis and reporting

  • Conduct data analysis and report when users have operation behaviors (such as clicking buttons and scrolling pages).
  • Called when processing dataJSON.stringifyIf the data volume is large, performance problems may occur.

At this point we can use requestIdleCallback to schedule reporting to avoid blocking page rendering. Here is a simple code example (skip).

const queues = [];
const btns = btns.forEach(btn= > {
    btn.addEventListener('click'.e= > {
        // do something
        pushQueue({
          type: 'click'
          // ...
        }));
        schedule(); // Wait until idle
    });
});
function schedule() {
    requestIdleCallback(deadline= > {
          while (deadline.timeRemaining() > 1) {
              const data = queues.pop();
              // Here you can process data and upload data
          }
          if (queues.length) schedule();
    });
}
Copy the code

preload

This is a little bit easier to understand, loading something in your free time, you can look at itqiankunTo preload JS and CSS, as shown below:There is also a pre-rendering, similar to 🐱.

Detection of caton

Generally, there are two kinds of stuck detection methods:

  • Measure the FPS value. If several consecutive FPS values are less than or equal to the threshold, it is considered to be stuck
  • Open a heartbeat detection between a worker thread and the main thread. If there is no response within a period of time, it is considered to be stuck

In retrospect, if a requestIdleCallback is not executed for a long period of time, there is no free time and it is most likely that something is stuck and can be reported. It is better for behavior stalling, for example: clicking on a button and adding our requestIdleCallback callback, if the callback is not executed for some time after the click, there is a high probability that the click caused the stalling.

Splitting time-consuming Tasks

This idea is vividly shown in the Scheduler Scheduler in React. Although React implements a set of scheduling logic (compatibility, stability, priority, etc.), it does not hinder our understanding. React transforms the diff process from recursion to iteration. Recursive diff on two large objects is a time-consuming task. It would be nice if we could break it down into smaller tasks. However, recursion can’t end in the middle, so React uses a data structure like Fiber, which turns recursion into a linked list iteration. The iteration can stop in the middle, so we don’t have to diff all at once. Ps: don’t know the list of students is simple to understand as an array, you think if we want to think about array traversal, we can finish one-time execution, but we can also split into several times after, as long as we record the index, next time come back to continue executing code is from the index began to traverse line, don’t know you get to the no.

2. Simple simulation of requestIdleCallback

At present, there are basically two methods for simulation:

Use setTimeout implementation

First, we need to know why setTimeout can be used to simulate, so let’s take a look at the following two lines of code:

// To some extent, the function is similar, and the writing style is similar
requestIdleCallback(() = > console.log(1));
setTimeout(() = > console.log(2));
Copy the code

If you know about setTimeout, you should know that it is not correct. It does not mean to execute immediately, but to execute as fast as possible, that is, wait until the main thread is empty and the microtask is finished, then it is the turn of setTimeout to execute. SetTimeout (fn) has an extra parameter in it:

window.requestIdleCallback = function(cb) {
    let start = Date.now();
    return setTimeout(function () {
      const deadline = { // This is to construct parameters
        timeRemaining: () = > Math.max(0.50 - (Date.now() - start)), // We write the remaining time within 50ms, which is the upper limit mentioned above. In fact, you can also write 40, 30, 16, 10, etc 😂
        didTimeout: false // Since we do not recommend using the timeout argument, we will simply write false here
      };
      cb(deadline);
    });
}
Copy the code

Note that this is not a polyfill of requestIdleCallback, because they are not actually the same. SetTimeout is not really using idle time, but executing your code as fast as conditions allow. The code above doesn’t limit itself to the free time of this frame like the real requestIdleCallback does, but it does two things: one is to segment the task and the other is to control the upper limit of time for each execution. Macros are generally the only tasks that satisfy these two conditions, so in addition to setTimout, postMessage is also possible. Now let’s look at another way to simulate.

RequestAnimationFrame + MessageChannel

let deadlineTime // End time of the current frame
let callback // The task that needs to be called back

let channel = new MessageChannel(); // A type of postMessage that has two and only ports and can send and receive events to and from each other.
let port1 = channel.port1;
let port2 = channel.port2;

port2.onmessage = () = > {
    const timeRemaining = () = > deadlineTime - performance.now();
    if (timeRemaining() > 1 && callback) {
        const deadline = { timeRemaining, didTimeout: false }; // Also construct a parameter herecallback(deadline); }}window.requestIdleCallback = function(cb) {
    requestAnimationFrame(rafStartTime= > {
        // Approximate expiration time = default this is the start time of a frame + approximate time of a frame
        deadlineTime = rafStartTime + 16
        callback = cb
        port1.postMessage(null);
    });
 }
Copy the code

This approach is slightly better than setTimeout because MessageChannel executes before setTimeout and does not have a minimum delay of 4ms. So why not use micromission simulation? Because if you use microtask emulation, after the code is finished, all the microtasks will continue to execute completely, and the main thread cannot be abandoned in time. Ps: Neither method is polyfill, just as close to the requestIdleCallback as possible, and the remaining time is also guesswork.

3. ⚠️ Precautions

Avoid changing the DOM in a callback

  • Since we are already using the post-render time, manipulating the DOM or reading the layout attributes of some elements in between will most likely result in re-rendering.
  • The time impact of manipulating the DOM is uncertain and can result in rearrangement and redrawing, so such operations are not controllable.
  • requestIdleCallbackDoes not align with frames (you should not expect this callback to be called on every frame), so dom manipulation is best left inrequestAnimationFrameRender 100,000 pieces of data as an example, like the following (skip) :
<div><button id="btn1">Render 100,000</button><input></div>
<div><button id="btn2">RequestIdleCallback render 100,000 pieces</button><input></div>
<ul id="list1"></ul>
<ul id="list2"></ul>

<script>
  // Plan 1: brainless addition
  const NUM1 = 100000;
  let list1 = document.getElementById("list1");
  document.getElementById("btn1").addEventListener('click', bigInsert1);
  function bigInsert1() {
    let i = 0;
    while (i < NUM1) {
      let item = document.createElement("li");
      item.innerText = The first `${i++}The data `; list1.appendChild(item); }}// Time slice
  const NUM2 = 100000
  let list2 = document.getElementById("list2");
  let f = document.createDocumentFragment();
  let i = 0;
  document.getElementById("btn2").addEventListener('click'.() = > {
    requestIdleCallback(bigInsert2);
  });
  function bigInsert2(deadline) {
    while (deadline.timeRemaining() > 1 && i < NUM2) {
      console.log('Idle in execution');
      let item = document.createElement("li");
      item.innerText = The first `${i++}The data `;
      f.appendChild(item);
      if (f.children.length >= 100) break; // Render 100 at a time
    }
    f.children.length && requestAnimationFrame(() = > {
      list2.appendChild(f);
      f = document.createDocumentFragment();
    });
    if (i < NUM2) requestIdleCallback(bigInsert2)
  }
</script>
Copy the code

So the code logic in requestIdleCallback should be predictable and controllable.

Avoid using promises in callbacks

Because the promise callback is a higher-priority microtask, it is executed immediately after the requestIdleCallback callback ends, potentially putting a timeout risk on this frame.

Use timeout only when needed

  • Using the timeout argument ensures that your code executes on time, but let’s think about itrequestIdleCallbackIt is supposed to be called at idle time. Using timeout gives you the feeling that I have no free time, and you force me to execute itrequestIdleCallback“, so it’s best to let the browser decide when to call it.
  • On the other hand, checking the timeout also incurs some extra overhead, and the API will be called more frequently. You can copy the following code to print it on the console (you can skip it) :
// There is no timeout, the general print value is 49/50 ms
function work(deadline) {
  console.log('Remaining time of current frame:${deadline.timeRemaining()}`);
  requestIdleCallback(work);
}
requestIdleCallback(work);
/ / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
// If there is a timeout, the print value is not fixed
function work(deadline) {
  console.log('Remaining time of current frame:${deadline.timeRemaining()}`);
  requestIdleCallback(work, { timeout: 1500 });
}
requestIdleCallback(work, { timeout: 1500 });
Copy the code

Get more free time

This is up to you to play!

  • Reduce render time (such as read-write separation, layering)
  • Break down the complex logic of JS and reduce the render generated by JS
  • .

The relevant test code can be viewed here at 👉🏻 : requestIdleCallback Test case reference article (wall crack recommendation) :

  • Using requestIdleCallback
  • Cooperative Scheduling of Background Tasks

If you have any questions, you can leave them in the comments section, haha 😄