React Source Code

    2. React Fiber architecture: core principles and implementation of scheduling

    Published
    November 20, 2022
    Reading Time
    2 min read
    Author
    Felix
    Access
    Public

    Before delving into the scheduling mechanism of React Fiber, we can get some inspiration from the requestIdleCallback API provided by the browser. Although this API is not the actual solution used by React, it provides a good basis for us to understand the concepts of "idle time" and "giving up the main thread".

    1. Introduction to requestIdleCallback

    The browser's rendering process is measured in frames, and generally we expect to maintain a refresh rate of 60 frames/second, which means that each frame takes about 16.67ms. If a frame's work is completed within 16ms, the remaining time is called "idle time".

    1.1 API detailed explanation

    const handle = window.requestIdleCallback(callback[, options])
    • callback: A function that is called during the idle period.
    • options: Optional, including the timeout attribute, specifying the timeout period.
    • Return value handle: can be used by cancelIdleCallback() to cancel the callback.

    The callback function receives an IdleDeadline object as a parameter, including:

    • timeRemaining(): Returns the estimated number of milliseconds remaining in the current idle period.
    • didTimeout: Boolean value indicating whether the callback was called due to timeout.

    1.2 Practical application examples

    Let’s use two comparative examples to gain a deeper understanding of the practical application of requestIdleCallback.

    Do not use requestIdleCallback

    const button = document.getElementById('button')
    const input = document.getElementById('input')
    let isPrimeNumber = true
    
    button.addEventListener('click', () => {
      //Simulation time-consuming calculation
      for (let i = 2; i < 1000000000; i++) {
        isPrimeNumber = true
        for (let j = 2; j < i; j++) {
          if (i % j === 0) {
            isPrimeNumber = false
            break
          }
        }
      }
      console.log('Complete calculation')
    })
    
    input.addEventListener('input', () => {
      console.log('Input value:', input.value)
    })

    In this example, the large amount of calculations after clicking the button will block the main thread, causing the input box to be unable to respond to user input in time.

    Use requestIdleCallback

    const button = document.getElementById('button')
    const input = document.getElementById('input')
    let isPrimeNumber = true
    let i = 2
    
    button.addEventListener('click', () => {
      function doWork(deadline) {
        while (i < 1000000000 && deadline.timeRemaining() > 0) {
          isPrimeNumber = true
          for (let j = 2; j < i; j++) {
            if (i % j === 0) {
              isPrimeNumber = false
              break
            }
          }
          i++
        }
    
        if (i < 1000000000) {
          requestIdleCallback(doWork)
        } else {
          console.log('Complete calculation')
        }
      }
    
      requestIdleCallback(doWork)
    })
    
    input.addEventListener('input', () => {
      console.log('Input value:', input.value)
    })

    This improved version divides the calculation task into multiple small tasks and executes them when the browser is idle, thereby ensuring the responsiveness of the input box.

    2. Scheduling implementation in React

    Although requestIdleCallback demonstrates the concept of idle dispatch, React does not use it directly. React implements its own idle scheduling, which is mainly based on the MessageChannel API. There are several important reasons for this choice:

    2.1 Why choose MessageChannel

    1. Efficiency: MessageChannel is a lightweight and efficient API, implemented natively by the browser, which is faster and more reliable than setTimeout or setInterval.

    2. Accuracy: Compared with setTimeout, MessageChannel can provide more precise timing. setTimeout usually has a minimum delay of 4ms, while MessageChannel has almost no such limitation.

    3. Priority: MessageChannel's message processing usually has a higher priority than setTimeout, and can be executed faster in a busy JavaScript environment.

    4. Performance advantage: According to performance testing, the delay of MessageChannel is usually between 0-1ms, while the setTimeout is between 4-16ms. This difference is especially obvious in scenarios that require frequent scheduling.

    5. Fine-grained control: Compared with requestAnimationFrame (usually 60Hz, about 16.7ms interval), MessageChannel can be triggered at any time between frames, providing finer-grained control.

    These features make MessageChannel an ideal choice for implementing the React scheduler, which can provide more fine-grained and controllable task scheduling capabilities, which is crucial for React to implement complex update scheduling and maintain application responsiveness.

    2.2 React scheduling source code implementation

    React's scheduler uses MessageChannel to send and receive messages. Here is a simplified version of the core implementation:

    let scheduledHostCallback = null
    let isMessageLoopRunning = false
    let yieldInterval = 5
    let deadline = 0
    
    const channel = new MessageChannel()
    const port = channel.port2
    channel.port1.onmessage = performWorkUntilDeadline
    
    const requestHostCallback = function (callback) {
      scheduledHostCallback = callback
      if (!isMessageLoopRunning) {
        isMessageLoopRunning = true
        port.postMessage(null)
      }
    }
    
    const performWorkUntilDeadline = () => {
      if (scheduledHostCallback !== null) {
        const currentTime = getCurrentTime()
        deadline = currentTime + yieldInterval
        const hasTimeRemaining = true
        try {
          const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime)
          if (!hasMoreWork) {
            isMessageLoopRunning = false
            scheduledHostCallback = null
          } else {
            port.postMessage(null)
          }
        } catch (error) {
          port.postMessage(null)
          throw error
        }
      } else {
        isMessageLoopRunning = false
      }
    }
    
    const shouldYieldToHost = function () {
      return getCurrentTime() >= deadline
    }

    The key points of this implementation are:

    1. Create a message channel using MessageChannel.
    2. The requestHostCallback function is used to register a callback and start the message loop.
    3. The performWorkUntilDeadline function is executed every time a message is received. It will run the callback function and decide whether it needs to continue working.
    4. The shouldYieldToHost function is used to determine whether the current work should be interrupted and the main thread should be given up.

    2.3 Time slice implementation

    React implements task division by setting a time slice (default is 5ms, see yieldInterval variable). In each time slice, React will try to perform as much work as possible, but once the time limit is exceeded, the current work will be paused and control will be returned to the browser.

    This implementation allows React to maintain the responsiveness of the application when performing complex updates, effectively preventing long-term tasks from blocking the main thread. Through the efficient nature of MessageChannel, React is able to perform scheduling multiple times in each frame, enabling smoother task segmentation and a more responsive user interface.

    3. setTimeout downgrade implementation

    Of course, in order to be compatible with environments that do not support MessageChannel, React also provides a downgrade implementation based on setTimeout:

    //Simplified version
    if (typeof window === 'undefined' || typeof MessageChannel !== 'function') {
      let _callback = null
      let _timeoutID = null
      const _flushCallback = function () {
        if (_callback !== null) {
          try {
            const currentTime = getCurrentTime()
            const hasRemainingTime = true
            _callback(hasRemainingTime, currentTime)
            _callback = null
          } catch (e) {
            setTimeout(_flushCallback, 0)
            throw e
          }
        }
      }
      requestHostCallback = function (cb) {
        if (_callback !== null) {
          setTimeout(requestHostCallback, 0, cb)
        } else {
          _callback = cb
          setTimeout(_flushCallback, 0)
        }
      }
      // ...degraded implementation of other related functions
    }

    This downgraded implementation, while not as precise as the MessageChannel version, still guarantees basic scheduling functionality.

    4. Summary and reflections

    React's scheduler is one of the core aspects of performance optimization. Through a customized scheduling mechanism, React can better control the timing and order of task execution, thereby maintaining good performance and responsiveness in complex applications.

    Understanding React's scheduling mechanism not only helps us use React better, but also provides us with valuable ideas for performance optimization in other scenarios.

    Then in the next chapter we will directly enter the explanation and study of task queue, interruptible rendering, scheduling, and synchronization mode.

    Comments

    Join the conversation

    0 comments
    Sign in to comment

    No comments yet. Be the first to add one.