# introduction

Read this article and you will learn:

  • Fully familiarize yourself with the use and value of requesTIDlecallback.

  • Clear usage scenarios for RequesTIDlecallBack.

  • Learn about the react Requestidlecallback polyfill implementation.

# Background

How does screen refresh rate relate to FPS?

Most of the current screen refresh rate is 60 Hz, that is, the screen refresh rate is 60 times per second. If the screen refresh rate is lower than 60 Hz, the human eye will perceive frame lag and drop. Similarly, the frame per second in our front-end browser refers to the number of times the browser refreshes per second. Between hardware refresh the screen, the browser is just a refresh (redraw), web page also will be very smooth, of course this is an ideal mode, if between hardware refreshes the browser re-paint is meaningless for many times, can only consume resources, if the browser redraw one time is hardware more refresh time, the human eye would perceive caton frame drop, etc., Therefore, the browser’s rendering of a redraw needs to be completed within 16ms (1000ms/60), which means that each redraw needs to be less than 16ms without getting stuck.

What needs to be done to redraw the browser at once?

How does the browser define a frame?

A browser frame says a complete redraw.

# knowrequestIdleCallback

The following demo source address

* * * * window. RequestIdleCallback () method will be called function in browser free time line.

API

var handle = window.requesTidlecallBack (callback[, options]) 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 IdleDeadline object contains didTimeout, which is a Boolean value to indicate whether the task times out and is used in conjunction with timeRemaining. TimeRemaining (), which represents the timeRemaining in the current frame. It can also be interpreted as how much time is left for the task. Options parameter timeout: indicates that if a task is not executed after the timeout period expires, the task is forcibly executed. If the callback has not been invoked by timeout milliseconds, it will be enforced during the next idle period. If the callback is explicitly executed within a certain period of time, you can set the timeout value. When the browser is busy, requestIdleCallback times out just like setTimeout.Copy the code

Return value: An identifier like the return value of setTimeout and setInterval. CancelIdleCallback can be cleared by cancelIdleCallback(Handle).

The free time

When does the browser run out of time?

The scene of a

When the browser takes less time to render a frame than the screen refresh rate (for a device with 60Hz, a frame interval should be less than 16ms), the idle time to start rendering the next frame, as shown in idle period,

Scenario 2

When the browser has no tasks to render, the main thread remains idle and the event queue is empty. To avoid user perceived delays in unpredictable tasks such as processing of user input, the length of these idle cycles should be limited to a maximum of 50ms, which is the timeRemaining of up to 50 times (20fps, which is why React polyfill is used), When the idle period ends, another idle period can be scheduled, and if it remains idle, the idle period will be longer, and background tasks can occur over a longer period of time. As shown in figure:

Note: The timeRemaining maximum is 50 milliseconds, based on RESPONSETIME research that indicates that responses to user input of less than 100 milliseconds are generally considered instantaneous to humans, meaning that they are undetectable to humans. Setting the idle deadline to 50ms means that even if user input occurs immediately after the idle task starts, the user agent still has a remaining 50ms in which to respond to user input without perceptible lag to the user.

#requestIdleCallbackusage

demo1:

First simulate a main-thread hogging method with predictable execution time:

function sleep(date) {
  let flag = true;
  const now = Date.now();
  while (flag) {
    if (Date.now() - now > date) {
      flag = false; }}}Copy the code

Execute the method called when the main thread is idle with requestIdleCallback:

function work() {
  sleep(2000); // Simulate the main thread task execution time

  requestIdleCallback((a)= > {
    console.log("Free time 1");
    sleep(1000);
    console.log("Idle time 1 callback task completed");
  });

  requestIdleCallback((a)= > {
    console.log("Free time 2");
  });
}

btn1.addEventListener("click", work);
Copy the code

Execution result: click button -> wait 2s -> Print idle time 1 -> wait 1s -> Print idle time 1 Callback task completed -> Idle time 2; When the sleep ends and requestIdleCallback gets the main thread free, cb (also performed on the main thread) is executed immediately and continues to occupy the main thread until the sleep ends. The second requestIdleCallback gets the main thread free and outputs the idle time 2. RequestIdleCallback can also be used with setTimeout. Of course, there are differences between them. The main thread time occupied by sleep simulation is controllable, but most of the time the main thread work is unpredictable. SetTimeout needs to know the specific delay time, so this is the main difference.

Demo2: Simulates DOM updates

function renderElement(txt) {
  const p = document.createElement("p");
  p.innerText = txt;
  
  return p;
}

let taskLen = 10;
let update = 0;
function work2() {
  document.body.appendChild(renderElement('There's still work to do${taskLen}`));
  console.log('Page update${++update}Time `);
  taskLen--;
  if (taskLen) {
    requestAnimationFrame(work2);
  }
}

btn1.addEventListener("click", () => {
  requestAnimationFrame(work2);
  window.requestIdleCallback((a)= > {
    console.log("Idle, requestIdleCallback in effect.");
  });
});
Copy the code

The result is shown below:

After performance recording analysis, see the figure below:

Zoom in on the first frame to see:

RequestIdleCallback is executed after the first frame because there is an idle period after the first frame. When will requestIdleCallback be executed if there is no free time in each frame?

Modify code:

. function work2() {document.body.appendChild(renderElement('There's still work to do${taskLen}`));
  console.log('Page update${++update}Time `);
  sleep(1000);
  taskLen--;
  if(taskLen) { requestAnimationFrame(work2); }}...Copy the code

Result: There is no free time until all render tasks are completed, so the CB of requestIdleCallback is executed last.

If you don’t want idle tasks to wait that long, then the second parameter of requestIdleCallback, {timeout: 1000}, will come in handy and change the demo as follows:

. btn1.addEventListener("click", () => {
  requestAnimationFrame(work2);
  window.requestIdleCallback(
    (a)= > {
      console.log("Idle, requestIdleCallback in effect.");
    },
    { timeout: 1200 }  // The latest can wait 1.2s); }); .Copy the code

Run the results, console output order:… -> Page updated 3 times -> Idle, requestIdleCallback in effect ->…

Demo3: User behavior

When user input is input, requestIdleCallback can be used to avoid delays caused by user actions that are not visible, such as sending data analysis, processing business logic that is not visible in the interface, etc.

The following takes sending data analysis as an example:

// Record the data queue that needs to be sent
const eventStack = [];
// Whether requestIdleCallback has been scheduled
let isRequestIdleCallbackScheduled = false;
// Simulate sending data
const sendData = (. arg) = > {
  console.log("Send data", arg);
};

function onDivThemeRed() {
  // Business logic
  render.classList.remove("border-blue");
  render.classList.add("border-red");

  eventStack.push({
    category: "button".action: "click".label: "theme".value: "red"}); schedulePendingEvents(); }function onDivThemeBlue() {
  // Business logic
  render.classList.remove("border-red");
  render.classList.add("border-blue");

  eventStack.push({
    category: "button".action: "click".label: "theme".value: "blue"}); schedulePendingEvents(); }function schedulePendingEvents() {
  if (isRequestIdleCallbackScheduled) return;

  isRequestIdleCallbackScheduled = true;

  requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
}

function processPendingAnalyticsEvents(deadline) {
  isRequestIdleCallbackScheduled = false;

  while (deadline.timeRemaining() > 0 && eventStack.length > 0) {
    const evt = eventStack.pop();

    sendData(
      "send"."event",
      evt.category,
      evt.action,
      evt.label,
      evt.value
    );
  }

  if (eventStack.length > 0) schedulePendingEvents();
}

btn2.addEventListener("click", onDivThemeRed);
btn3.addEventListener("click", onDivThemeBlue);
Copy the code

Conclusion:

The requestIdleCallback is executed at the end of each frame to determine whether the browser is idle. If the browser is always occupied, there is no idle time, and if requestIdleCallback is not set to timeout, the callback task will always be delayed. If timeout is set in the current frame, the browser will determine whether to execute the callback after the current frame ends. In principle, the FPS of requestIdleCallback is only 20, so tasks with high FPS requirements need to be aligned with the render frame, such as DOM animation, etc. RequestAnimationFrame is recommended for optimal fluency.

React requestIdleCallback

#reactrequestIdleCallback pollyfillThe implementation of the

As mentioned earlier, requestIdleCallback only works at 20FPS, but it usually works at 60FPS, which is 16.7ms per frame. This is why the React team implemented requestIdleCallback themselves. A postMessage is triggered when requestAnimationFrame starts to get a frame, and idleTick is called when idle to complete the asynchronous task.

React requestIdleCallback

Source in the packages/scheduler/SRC/forks/SchedulerHostConfig default. Js, respectively to the DOM and DOM have different implementation of the environment.

export let requestHostCallback; A similar requestIdleCallback / /
export let cancelHostCallback; A similar cancelIdleCallback / /
export let requestHostTimeout; // Non-DOM environment implementation
export let cancelHostTimeout;  / / cancel requestHostTimeout
export let shouldYieldToHost;  // Check whether the task timed out and needs to be interrupted
export let requestPaint; // 
export let getCurrentTime; // Get the current time
export let forceFrameRate; // Calculate frame time according to FPS
// Non-DOM environment
if (typeof window= = ='undefined' || typeofMessageChannel ! = ='function') {
	let _callback = null; // The callback being executed
  let _timeoutID = null;
  const _flushCallback = function() {
    // Execute if the callback exists.
    if(_callback ! = =null) {
      try {
        const currentTime = getCurrentTime();
        const hasRemainingTime = true;
        // hasRemainingTime similar to deadline.didTimeout
        _callback(hasRemainingTime, currentTime);
        _callback = null;
      } catch (e) {
        setTimeout(_flushCallback, 0);
        throwe; }}};// ...
  
  requestHostCallback = function(cb) {
    // If _callback exists, the task will continue.
    if(_callback ! = =null) {
      // The third parameter of setTimeout can delay the execution of the task.
      setTimeout(requestHostCallback, 0, cb);
    } else {
      // Otherwise, execute directly.
      _callback = cb;
      setTimeout(_flushCallback, 0); }}; cancelHostCallback =function() {
    _callback = null;
  };
  requestHostTimeout = function(cb, ms) {
    _timeoutID = setTimeout(cb, ms);
  };
  cancelHostTimeout = function() {
    clearTimeout(_timeoutID);
  };
  shouldYieldToHost = function() {
    return false;
  };
  requestPaint = forceFrameRate = function() {};
} else {
  Performance, requestAnimationFrame, cancelAnimationFrame
  // ...
  const performWorkUntilDeadline = (a)= > {
    if(scheduledHostCallback ! = =null) {
      const currentTime = getCurrentTime();
      // yieldInterval Specifies the time of each frame. The deadline indicates the deadline
      deadline = currentTime + yieldInterval;
      const hasTimeRemaining = true;
      try {
        const hasMoreWork = scheduledHostCallback(
          hasTimeRemaining,
          currentTime,
        );
        if(! hasMoreWork) { isMessageLoopRunning =false;
          scheduledHostCallback = null;
        } else {
          If there is more work, schedule the next message event at the end of the previous message event
          port.postMessage(null); }}catch (error) {
        // If the scheduling task throws, exit the current browser task so that you can observe the error.
        port.postMessage(null);
        throwerror; }}else {
      isMessageLoopRunning = false;
    }
    needsPaint = false;
  };

  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;

  requestHostCallback = function(callback) {
    scheduledHostCallback = callback;
    if(! isMessageLoopRunning) { isMessageLoopRunning =true;
        port.postMessage(null); }}; }Copy the code

It can be seen from the above that requestHostCallback is implemented by setTimeout simulation in non-DOM mode, while postMessage and onMessage publishing and subscription modes based on MessageChannel message are implemented in DOM mode.

# summary

RequestIdleCallback:

  • RequestIdleCallback is performed after the screen has been rendered.

  • Some low-priority tasks can be performed when the browser is not busy, such as requestIdleCallback. Meanwhile, due to the limited time, the tasks it performs should be quantified as much as possible. Micro Tasks are more suitable for requestIdleCallback.

  • The requestIdleCallback does not align with frames, so DOM manipulation and animation are best performed in the requestAnimationFrame, which is performed before the screen is rerendered.

  • Promise is not recommended to do this either, because the higher-priority microtask in the Event Loop, the Promise callback property, is executed immediately after the requestIdleCallback ends, regardless of whether there is any spare time. This has a high probability of making a frame over 16 ms.

# develop

requestAnimationFrame

MessageChannel


Welcome your criticism and correction.

The source address

🐶 🐶 🐶 🐶 🐶 🐶

Reference links:

The w3c. Making. IO/requestidle…

Developers.google.com/web/updates…

Wiki.developer.mozilla.org/zh-CN/docs/…

Juejin. Cn/post / 684490…