Before the order

  • Does requestAnimationFrame really understand?

Happy New Year, everyone. As we start the New Year, let’s take a look at one of the most common, important, but not always used knowledge points: requestIdleCallback

This article will solve your questions:

  • What exactly is requestIdleCallback?
  • How does requestIdleCallback relate to the React Fiber architecture?
  • How does requestIdleCallback help with performance tuning?

If this article is helpful to you, please add the author mokinzhao on wechat and enter the group for in-depth communication

What is requestIdleCallback?

RequestIdleCallback is an experimental API that lets you do things when your browser is idle

RequestIdleCallback Simply says that if a frame has free time, perform a task. The purpose is to solve the page frame loss (stuck) situation when tasks need to occupy the main process for a long time, resulting in higher priority tasks (such as animation or event tasks), unable to respond in time. Therefore, RequestIdleCallback localization deals with tasks that are not important or urgent.

The basic grammar

var handle = window.requestIdleCallback(callback[, options])
Copy the code
  • The return value

An ID, you can put it into the Window. The cancelIdleCallback () method to end a callback.

  • callback

A reference to a function to be called when the event loop is idle. The function receives a parameter called IdleDeadline, which gets the status of the current idle time and whether the callback has been executed before the timeout.

  • The options of the optional

Includes optional configuration parameters. Has the following properties: TIMEOUT: If timeout is specified and there is a positive value, and the callback has not been called after Timeout milliseconds, the callback task will be queued in an event loop, even if doing so may have a negative impact on performance.

  • The basic application
type Deadline = {
  timeRemaining: () = > number // The current available time. That's the remaining time of the frame.
  didTimeout: boolean // Whether timeout occurs.
}

function work(deadline:Deadline) { // Deadline has a method called timeRemaining() to retrieve the remaining free time of the current browser,Unit ms; There's a property didTimeout that says whether or not you're going to time outconsole.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

disadvantages

  • This is an experimental feature

Some browsers are still developing this feature, please refer to the browser compatibility table to see which prefixes are appropriate for different browsers. The syntax and behavior of this feature may change in future versions of browsers as the standard documentation for this feature may be revised.

  • The experimental process

Conclusion: The FPS of requestIdleCallback is only 20ms, and the normal rendering time is controlled at 16.67ms (1s / 60 = 16.67ms). The time is higher than the page fluency appeals.

  • Some people think that

RequestIdleCallback is mainly used for unimportant and non-urgent tasks because React renders content that is not unimportant and non-urgent. The React team implemented the API on their own because it was only moderately compatible with frame rendering and didn’t quite meet the requirements of rendering

Difference between requestIdleCallback and requestAnimationFrame

The requestAnimationFrame callback is confirmed every frame and is a high priority task. The callback to requestIdleCallback is not necessarily a low-priority task. The page we see is drawn frame-by-frame by the browser, and FPS is usually smooth at 60, while FPS is slow at low FPS. So what does the browser do in each frame, as shown below:

One frame includes user interaction, JavaScript script execution; ***requestAnimationFrame(rAF)*** calls, layout calculations, page redrawing etc. If a frame does not perform a lot of tasks and the above tasks are completed in less than 16.66ms(1000/60), then the frame will have some free time to execute the requestIdleCallback callback, as shown in the figure below:

When the application stack is empty and the browser is idle, the time allowed for requestIdleCallback execution can be appropriately extended, up to 50ms, to prevent unpredictable tasks (such as user input) and avoid delays perceived by users when they cannot respond in a timely manner.

Since requestIdleCallback takes advantage of the frame’s idle time, it is possible that the browser is always busy and the callback cannot be executed. In this case, you need to pass the second configuration parameter timeout when calling requestIdleCallback.

requestIdleCallback(myNonEssentialWork, { timeout: 2000 });

function myNonEssentialWork(deadline) {
  Deadline. didTimeout is true if the callback is executed because of a timeout
  while (
    (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
    tasks.length > 0
  ) {
    doWorkIfNeeded();
  }
  if (tasks.length > 0) { requestIdleCallback(myNonEssentialWork); }}Copy the code

If the timeout callback is executed, the user may feel stuck because a frame is longer than 16ms.

RequestIdleCallback what you can and can’t do in your free time

What not to Do

  1. Do not perform DOM modification operations in requestIdleCallback

It is strongly recommended not to perform DOM modification operations in requestIdleCallback. As you can see from the composition of the above frame, the style changes, layout calculations, and so on are completed before the requestIdleCallback callback is executed. If you modify the DOM in the callback, the previous layout calculation is invalidated. And if there are operations related to retrieving the layout in the next frame, the browser will have to force a rearrangement, which can have a significant impact on performance. In addition, because the time to modify the DOM is unpredictable, it is easy to exceed the current frame free threshold.

The recommended approach is to make DOM changes in the requestAnimationFrame.

  1. In addition to DOM modification, **Promise’s resolve(reject)** is also not recommended because Promise’s callback executes immediately after Idle’s callback completes. The promise callback is a high-priority microtask, so it will be executed immediately after the requestIdleCallback callback ends, potentially putting a timeout on this frame.

What can do

  1. Data analysis and reporting
  • Conduct data analysis and report when users have operation behaviors (such as clicking buttons and scrolling pages).
  • Json.stringify is often called when processing data, which can cause performance problems if the data volume is large.

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
  1. preload

To load something in your spare time, take a look at an example from Qiankun that preloads JS and CSS

function prefetch(entry: Entry, opts? : ImportEntryOpts) :void {
  if(! navigator.onLine || isSlowNetwork) {// Don't prefetch if in a slow network or offline
    return;
  }

  requestIdleCallback(async() = > {const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
    requestIdleCallback(getExternalStyleSheets);
    requestIdleCallback(getExternalScripts);
  });
}
Copy the code
  1. 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.

  1. 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.

Analog implementation of requestIdleCallback

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.

How does requestIdleCallback relate to time slicing in React

The RequestIdleCallback experiment case

  • Conclusion:
    • RequestIdleCallback uses the idle time between frames to execute JS
    • RequestIdleCallback comes after Layout and Paint, which means that requestIdleCallback can be js computed and DOM altered, which means that layout and Paint will be rearranged
    • RequestAnimationFrame comes before Layout and Paint, so it’s better to change DOM operations.
    • Hence the React internal implementation of the scheduling policy is also based on requestAnimationFrame
  • Reason:
    • RequestIdleCallback is positioned to handle unimportant and non-urgent things. Because React renders content, it’s not that it’s unimportant and not urgent. The React team implemented the API on their own because it was only moderately compatible with frame rendering and didn’t quite meet the requirements of rendering

React source code requestHostCallback

  • SchedulerHostConfig.js

  • Execute macro task (callback task)

    • RequestHostCallback: Triggers a macro task performWorkUntilDeadline.
    • PerformWorkUntilDeadline: macro task processing.
      • Whether there is plenty of time, there is implementation.
      • Check whether there is another callback task after this callback task is executed, that is, check hasMoreWork.
      • PostMessage (null);
  let scheduledHostCallback = null;

  let isMessageLoopRunning = false;

  const channel = new MessageChannel();

  / / sent port2
  const port = channel.port2;
  / / received port1
  channel.port1.onmessage = performWorkUntilDeadline;
  const performWorkUntilDeadline = () = > {
    // There is an execution task
    if(scheduledHostCallback ! = =null) {
      const currentTime = getCurrentTime();
      // Yield after `yieldInterval` ms, regardless of where we are in the vsync

      // cycle. This means there's always time remaining at the beginning of
      // the message event.

      // Calculate the expiration point of a frame
      deadline = currentTime + yieldInterval;
      const hasTimeRemaining = true;
      try {
        // After executing the callback, determine whether there are other tasks to follow
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );

        if(! hasMoreWork) { isMessageLoopRunning =false;
          scheduledHostCallback = null;

        } else {
          // If there's more work, schedule the next message event at the end
          // of the preceding one.
          // There are other tasks to push into the next macro task queue
          port.postMessage(null); }}catch (error) {
        // If a scheduler task throws, exit the current browser task so the
        // error can be observed.
        port.postMessage(null);
        throwerror; }}else {
      isMessageLoopRunning = false;
    }
    // Yielding to the browser will give it a chance to paint, so we can
    // reset this.
    needsPaint = false;

  };

  // requestHostCallback executes tasks in a frame
  requestHostCallback = function(callback) {
    // Callback registration
    scheduledHostCallback = callback;

    if(! isMessageLoopRunning) { isMessageLoopRunning =true;
      // Enter the macro task queue
      port.postMessage(null); }}; cancelHostCallback =function() {
    scheduledHostCallback = null;

  };
Copy the code

The author established a full stack large front-end communication group, like to discuss hot technology, please add wechat: Mokinzhao

  • Front-end technology summary: full stack big front-end column

reference

requestIdleCallback-MDN

Take a closer look at the requestIdleCallback

React requestIdleCallback scheduler