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 thetimeoutattribute, specifying the timeout period.- Return value
handle: can be used bycancelIdleCallback()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
Efficiency: MessageChannel is a lightweight and efficient API, implemented natively by the browser, which is faster and more reliable than setTimeout or setInterval.
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.
Priority: MessageChannel's message processing usually has a higher priority than setTimeout, and can be executed faster in a busy JavaScript environment.
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.
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:
- Create a message channel using
MessageChannel. - The
requestHostCallbackfunction is used to register a callback and start the message loop. - The
performWorkUntilDeadlinefunction is executed every time a message is received. It will run the callback function and decide whether it needs to continue working. - The
shouldYieldToHostfunction 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.