Starting with V16.0.0, React implements an asynchronous render Mode that addresses the previous issue of synchronous updates to the Stack Reconciler. Asynchronous rendering is essentially breaking up a large rendering task into smaller tasks and handing over the main thread at the end of each task so that other threads can do other things (rendering, responding to user clicks, etc.). The most common way to do this is to use setTimeout to add these small tasks to an event queue and use the browser’s Event loop to execute them asynchronously. React V16.0.0 implements requestIdleCallback Polyfill.

To maximize rendering performance, React wants these small tasks to be performed at idle browser time. The browser provides requestIdleCallback to ensure that you can perform some operations during idle time. However, it has compatibility issues. For browsers that do not support rIC, the React team uses requestAnimationFrame and postMessage, and automatically adjusts the frame rate. Here is the implementation logic,

First define special Message events and listen for message events,

// Define the message event name
var messageKey =
  "__reactIdleCallback$" +
  Math.random()
    .toString(36)
    .slice(2)

/ / to monitor the message
window.addEventListener("message", idleTick, false)
Copy the code

For rIC implementations, rAF is used to ensure that the next frame is triggered

rIC = function(callback) {
  // The callback is stored first
  scheduledRICCallback = callback
  // Check whether rAF scheduling is underway
  if(! isAnimationFrameScheduled) { isAnimationFrameScheduled =true
    requestAnimationFrame(animationTick)
  }
  return 0
}
Copy the code

The callback that needs to be executed is saved, and then the rAF is triggered.

var animationTick = function(rafTime) {
  isAnimationFrameScheduled = false
  if(! isIdleScheduled) { isIdleScheduled =true
    window.postMessage(messageKey, "*")}}Copy the code

In rAF trigger function, call postMessage, trigger message event.

var idleTick = function(event) {
  // Check for internally triggered message,
  if(event.source ! = =window|| event.data ! == messageKey) {return
  }
  isIdleScheduled = false
  // Retrieve the previously stored callback
  var callback = scheduledRICCallback
  scheduledRICCallback = null
  if(callback ! = =null) {
    // Actually execute callback
    callback(frameDeadlineObject)
  }
}
Copy the code

In the message event, the callback is actually executed. After looking at the implementation, there are a couple of questions,

  1. Why not just use rAF?
  2. Why not just use itpostMessage?
  3. Why not usesetTimeout?

If you use rAF to achieve

We know that rIC is triggered at the end of the current frame if there is time left in the frame, or if the current page is not updated and is idle, rIC is also triggered. RAF is triggered at the beginning of a frame.

If rAF is directly used, the update operation of Fiber Tree will be performed in rAF, which increases the time required to complete the current frame overall, resulting in the decrease of frame rate.

If you use postMessage

PostMessage is commonly used to communicate between parent pages of different domains (using iframe), and you can check the MDN documentation for details. But in this case, its use does not involve cross-domain communication, just asynchronous invocation. If you just use postMessage, the code looks like this,

var scheduledRICCallback = null
var isIdleScheduled = false

window.addEventListener("message", idleTick, false)

var idleTick = event= > {
  if(event.source ! = =window|| event.data ! == messageKey) {return
  }
  isIdleScheduled = false
  var callback = scheduledRICCallback
  scheduledRICCallback = null
  if(callback ! = =null) {
    var start = Date.now()
    callback({
      didTimeout: false.timeRemaining: function() {
        // Is there maximum value that timeRemaining() will return? Yes, it 's currently 50 ms
        // see: https://developers.google.com/web/updates/2015/08/using-requestidlecallback#faq
        return Math.max(0.50 - (Date.now() - start))
      },
    })
  }
}

var ric = callback= > {
  scheduledRICCallback = callback
  if(! isIdleScheduled) { isIdleScheduled =true
    window.postMessage(messageKey, "*")}}Copy the code

The main problem with this implementation is that the calculation of the remaining time of the current frame is inaccurate. It always defaults to 50ms remaining time. At 60fps, the execution time of a frame is around 17ms. So the React team used rAF to dynamically calculate the current frame rate and calculate the current frame execution deadline. The logic for dynamically calculating the current frame rate is roughly as follows,

  1. Starting with the current frame rate of 30fps, the execution time of each frame is approximately 33ms
  2. If the execution time of two consecutive frames is shorter than the execution time of each frame at the current frame rate, the current frame rate needs to be improved
// Frame expiration time
var frameDeadline = 0
// the initial current frame rate is 30fps, so the frame execution time is 33ms
// Last frame execution time
var previousFrameTime = 33
// execution time per frame at 30fps
var activeFrameTime = 33

// Calculate the rIC parameter, the remaining time
var frameDeadlineObject = {
  timeRemaining:
    typeof performance === "object" && typeof performance.now === "function"
      ? function() {
          return frameDeadline - performance.now()
        }
      : function() {
          return frameDeadline - Date.now()
        },
}

var animationTick = function(rafTime) {
  // Calculate the execution time of the next frame, where frameDeadline is the expiration time of the previous frame
  var nextFrameTime = rafTime - frameDeadline + activeFrameTime
  // If the execution time of two consecutive frames is less than the frame execution time, the frame rate can be increased
  if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
    if (nextFrameTime < 8) {
      // Maximum increased 120fps,
      nextFrameTime = 8
    }
    // The execution time of two consecutive frames is larger to prevent the execution from exceeding the frame deadline
    activeFrameTime =
      nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime
  } else {
    previousFrameTime = nextFrameTime
  }
  // Calculate the frame cutoff time
  frameDeadline = rafTime + activeFrameTime
}
Copy the code

The React implementation does not deal with scenario 1, that is, it does not reduce the frame rate.

  1. In the first scenario, the start time of the next frame is longer than the end time of the previous frame, indicating that the estimated frame execution time is too small, and the frame rate can be adjusted and reduced
  2. In the second scenario, the start time of the next frame is equal to the end time of the previous frame, indicating that the estimated frame execution time is correct and the frame rate does not need to be adjusted
  3. In the third scenario, the start time of the next frame is less than the end time of the last frame, indicating that the estimated frame execution time is too large, and the frame rate can be adjusted and increased

Why not use setTimeout

Since postMessage is used for asynchronous scheduling, why not use setTimeout, which can also be used for asynchronous scheduling?

Without rIC, we can’t really accurately determine the frame idle time. PostMessage is used to implement asynchronous scheduling in order to hand over js thread execution to render related threads. Above, rAF is used to dynamically calculate the current frame rate and get the frame cutoff time, so that the parameter timeRemaining in rIC can be accurately calculated. Note that the frame cutoff time is calculated in rAF based on the start time of the current frame, that is, we default the callback to execute after the frame cutoff time is calculated. However, as mentioned earlier, we cannot execute callback directly in rAF, instead using asynchronous scheduling. The earlier asynchronous scheduling starts, the less error the timeRemaining values will be. For example, if the start time of the frame is 10ms, the end time of the frame calculated based on 10ms is 30ms, that is, the total execution time of the frame will be 20ms, if the asynchronous scheduling is 5ms after the actual execution of the callback, Then calling timeRemaining in the callback results in only 30-5 = 25 times, which is 5ms less than expected.

To reduce error, React uses postMessage instead of setTimeout because postMessage is executed earlier than setTimeout. For modern browsers, using setTimeout(callback,0) has a 4ms execution interval limit.

Html.spec.whatwg.org/multipage/t…

  1. If timeout is less than 0, then set timeout to 0.
  2. If nesting level is greater than 5, and timeout is less than 4, then set timeout to 4.

Consider the following code: call setTimeout in setTimeout. After cb executes 5 times, subsequent CB must execute at least 4ms apart.

Note: 4 Ms is specified by the HTML5 spec and is consistent across Browsers released in 2010 and onward. Prior to (Firefox 5.0) / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.

function cb() {
  setTimeout(cb, 0)}setTimeout(cb, 0)
Copy the code

Using postMessage can be executed earlier than setTimeout, as described below on MDN

To implement a 0 ms timeout in a modern browser, you can use window.postMessage() as described here.

The author of the React team’s implementation of this polyfill also explains,

I originally used setTimeout(idle, 0) but since it adjusts to at least 4ms and sometimes more, you end up dropping a bit more than I’d like from the available frame time. However, in practice postMessage can be delayed up to 4ms anyway because of the internal browser scheduling around the frame.

I have also tested Chrome(84.0.4147.125) as follows:

summary

React V16.0.0 uses rAF to dynamically calculate frame rates and postMessage to implement asynchronous calls. Of course, there are some scenarios that are not considered in its implementation, there are some shortcomings,

  1. The rAF will be optimized by the browser and will not trigger rAF in some cases, such as when switching to the background; If rAF is not executed, then callback will not be fired as expected.
  2. It supports only a single callback execution, if multiplerIC(callback)Only the last callback will be executed, which does not really conform to the rIC specification. inMultiple callbacks are supported in V16.4.0.
  3. The current implementation does not support ittimeoutParameters,This is done in V16.2.0
  4. Currently not supportedcancelIdleCallbackIn theThis is implemented in V16.4.0

data

  • RequestIdleCallback polyfill – React v16.0.0
  • Minimum delay and timeout nesting
  • setTimeout with a shorter delay

–EOF–

This is my secret base 🙂