I recently read the React18 source code on GitHub and learned some exciting things. In this post, I want to describe how React18 works, by using figures.
I have also included GitHub links in some sections for readers who want to look at the code.
Fiber
First of all, I will explain what Fiber is. If you are interested in React, you may have heard the word "Fiber" somewhere. However, there are few documents and articles that explain Fiber.
What is Fiber?
In brief, one of the Fibers means one of the components, such as <MyComponent>
and <div>
. React builds a Fiber tree (it's like a DOM tree) for calculating where it has changed due to user interactions.
Besides, Fiber also means a unit of the task of the queue. React can pause the rendering per Fiber.
Why does React use the Fiber tree?
This is because React can more easily manage the data associated with a component, such as the priority, the component name, etc., than using a DOM tree.
What is the structure of the Fiber tree?
Let's see what the structure of the Fiber tree is, I have prepared a sample code below to describe it.
<html>
<body>
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
<div id="root"><!-- React will inject elements here --></div>
<script>
const App = () => {
const child = React.createElement('span', null, 'world');
return React.createElement('div', null, 'Hello', child);
}
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(React.createElement(App, {}, null));
</script>
</body>
</html>
If you run the code, React will create the Fiber tree like this.
As you can see, This tree is almost the same as a DOM tree.
However, it has some differences from a DOM tree, like the one below.
- The Fiber tree contains custom components such as
<App>
. - There are HostRoot and FiberRootNode components in the tree.
So, what are HostRoot and FiberNode?
FiberNode
FiberNode is the node indicated by the green circle in the figure. It has some properties named child
, sibling
, and return
. These properties are for accessing its child, sibling, and parent.
FiberRootNode
FiberRootNode is the node indicated by the red circle in the figure. Its structure is entirely different from FiberNode
, and it has a property called containerInfo
that allows React to access the root element by accessing this property.
How does React create the Fiber tree?
Then, let's see how React creates the Fiber-tree.
I describe the rendering process using the sample code below and have added comments at key points in the code.
<html>
<body>
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
<div id="root"><!-- React will inject elements here --></div>
<script>
const App = () => {
const child = React.createElement('span', null, 'world');
return React.createElement('div', null, 'Hello', child);
}
// ★1 Create the FiberRootNode and the HostRoot
const root = ReactDOM.createRoot(
document.getElementById('root')
);
// ★2 Create the ReactElement of `<App>`
reactElement = React.createElement(App, {}, null)
// ★3 Start render process
root.render(reactElement);
</script>
</body>
</html>
★1 Create the FiberRootNode and the HostRoot
When React calls the ReactDOM.createRoot
method, it invokes the function named createFiberRoot
to create the FiberRootNode and the HostRoot.
You can look at the code on GitHub.
After creating the FiberRootNode and the HostRoot, the Fiber tree looks like this.
★2 Create the ReactElement of <App>
Then, React creates the ReactElement, which is required for calling the root.render
function.
Note that this object structure is completely different from Fiber. If you use the JSX format, you typically don't need to use the React.createElement
method because a compiler like Webpack will convert JSX to the React.createElement
at compile time. (docs)
★3 Start rendering
Finally, the root.render
function is called, and React starts rendering. This process has three steps.
- React recursively converts ReactElements to Fibers and adds them to the Fiber tree until it reaches the terminal node.
- When React reaches the terminal node, it returns to the parent while saving a DOM element which is made from a Fiber to the property named
stateNode
. - In the end, when React reaches the root, it appends the completed DOM elements to
div#root
with appendChild.
The explanation is complicated, so I made a GIF image to make it easier to understand.
The stateNode
property generally holds the DOM element, including itself and its children, but functional components (such as <App>
) don't have stateNode
. Their stateNode
property is always null
.
Finally, the function finds the closest stateNode
(except for null
) from the root and appends it to the div#root
. Then, a user can see the message "Helloworld" on the screen.
Rendering phase and Commit phase
I explained the flow of the rendering process in the previous section. In this section, I will explain the rendering process in more detail.
Two phases in the rendering process
The rendering process can be divided into two phases, the rendering phase, and the commit phase.
In the rendering phase, React converts ReactElements to DOM elements and stores them in stateNode
. Then, in the commit phase, React appends the DOM elements to div#root
.
It means React doesn't change DOM elements during the rendering phase, and any changes are never shown on the screen until the commit phase starts. So, React can pause or resume the rendering phase anytime if it takes longer.
This behavior provides a good user experience because React doesn't block the JavaScript thread for long. JavaScript is a single-threaded language, and if React blocks the thread for too long, the user may think the browser has frozen up.
On the other hand, in the commit phase, React can block the JavaScript thread. This is not good for the user experience. However, React just updates DOM elements based on the Fiber tree in the commit phase, so React completes its tasks quickly and releases the thread.
In what cases does React pause the rendering phase?
I was curious about this and looked into cases of the rendering phase can be paused. According to this issue below, React can only pause the rendering phase only if we use startTransition or Suspense.
Bug: time slice not work in react 18 #24392
in react@16.8.0, a long task will be sliced multi short task, demo: https://stackblitz.com/edit/react-ts-aqwejz
but in react@18.0.0, it will be only a long task, demo: https://stackblitz.com/edit/react-ts-ezgtzn
Is it a react18 time slice bug or feature?
React version: 16.8.0 & 18.0.0
Steps To Reproduce
- run a example app like below
- open inspector -> performance, then record and analyze
Link to code example: 16.8.0: https://stackblitz.com/edit/react-ts-aqwejz 18.0.0: https://stackblitz.com/edit/react-ts-ezgtzn
The current behavior
in react@18.0.0, long task not be sliced
The expected behavior
in react@18.0.0, long task will be sliced
So if you want to allow React to pause the render phases, you need to wrap your code with startTransition or Suspense, like this.
React.startTransition(() => {
root.render(React.createElement(App, {}, null));
})
Then, How does React start the rendering phase inside?
React calls the workLoopConcurrent
or workLoopSync
function to start the rendering phase. Both functions start the rendering phase, but workLoopConcurrent
can pause it, and workLoopSync
cannot.
The workLoopConcurrent
function calls the shouldYield
function to check whether React should pause the rendering phase. The shouldYield
function returns true if more than 5ms has passed since the phase start.
You can look at the code on GitHub.
How does React decide which function to call? It depends on Lane.
What is Lane?
Briefly, Lane is a 32-bit mask flag. Each bit represents a priority and a type of task. The closer the lane is to '0', the higher the priority.
Also, the NoLane
is a special one. Basically, this Lane is for a default value.
You can take a look at the list of lanes below.
ReactFiberLane.old.js (GitHub)
As you can see, the lane is just an integer. React can use bitwise operators to create a bitmask containing some lanes or check if the bitmask has a particular lane.
const SyncLane = 0b0000000000000000000000000000001;
const InputContinuousLane = 0b0000000000000000000000000000100;
// Merge lanes by using `|` operator
// Returns a bitmask containing `SyncLane` and `InputContinuousLane`.
const merged = SyncLane | InputContinuousLane;
console.log(merged); // 0b0000000000000000000000000000101
// Check if the bitmask have `InputContinuousLane`
console.log((merged & InputContinuousLane) != 0); // true
In the rendering process, React has to handle a lot of flags. The bitmask is a good solution for managing flags, compared to having them with individual variables.
How does React choose lanes?
There are several ways that React can select a lane, but one of the simplest is to select a lane based on an event type, such as a click event.
Let's look at the sample code below. When you click the button, the text in the button changes from "Click" to "Clicked".
const App = () => {
const [test, setTest] = React.useState('Click')
const onClick = () => {
setTest('Clicked')
}
return React.createElement('button', { onClick }, test);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App, {}, null));
After the page loaded, React calls the listenToAllSupportedEvents
function to add many event listeners, allowing React to catch almost all user events. You can look at these listeners in the DevTools.
When you click a button, the above event listener is fired before the onClick callback is triggered, and React decides the lane based on an event type. In the case of the sample code, React selects the SyncLane
when a click event occurs. It means that the click event is a high priority.
This is because a user generally wants the browser to respond quickly when the user clicks something. On the other hand, a mouse-move event is not higher than a click event.
You can look at the code on GitHub.
I looked at the usage of lanes in the source code and made a list. I put "unknown" in the place where I couldn't find the usage.
Name | Usage |
---|---|
NoLane | Default value |
SyncLane | ClickEvent, MouseMoveEvent, etc. |
InputContinuousHydrationLane | Unknown. It is probably used for Suspense and hydrateroot |
InputContinuousLane | MouseEnter, etc. |
DefaultHydrationLane | Unknown. It is probably used for Suspense and hydrateroot |
DefaultLane | It is mainly used for initial rendering. |
TransitionHydrationLane | Unknown. It is probably used for Suspense and hydrateroot |
TransitionLane1~16 | It is used for startTransition . React uses a different number each time, and if React previously used TransitionLane16, it will use TransitionLane1. |
RetryLane1~5 | React uses it when Suspense is still loading. |
IdleHydrationLane | Unknown. It is probably used for Suspense and hydrateroot |
IdleLane | Unknown |
OffscreenLane | Unknown. It's probably used for the offscreen feature, which is not implemented yet. |
How does React use lanes?
So how does React use lanes? It is not easy to list all the uses, because React uses them in many different ways, but the main uses are as follows.
- React uses the lanes contained in each Fiber to determine if a Fiber has any updates and should be re-rendered.
- React uses a lane to decide if it can pause the rendering phase. (This is what I described in the previous section.)
About the scheduling
In the previous section, I described the rendering phase and the lane. In this section, I will explain the scheduling that React has.
React has a scheduling system. The scheduler queues the rendering phase as one of the tasks and invokes it with a delay. The scheduler queues the rendering phase when the initial load or any event occurs, such as a click event.
And interestingly, SyncLine (used for click events, etc.) is special because React chooses a scheduler system based on whether the current lane is SyncLine or not.
Let's see what the difference is between SyncLine and other lanes.
Case 1. non-SyncLane
1. Create and queue a task.
After any events or an initial page load happens, React creates a task object like the one below.
var newTask = {
id: // The task id. It is used to sort tasks.
callback: // This property holds the function called when the task is dequeued, such as the function to start the rendering process.
priorityLevel: // Unknown.
startTime: // The time this task was performed.
expirationTime: // The expiration time. The higher the lane priority, the shorter it is. When it expires, React executes it without pausing.
sortIndex: // The index for sorting tasks. Basically, its value is the same as startTime. If different tasks have the same sortIndex, React will use `id` instead.
};
Then, React pushes it into the array for managing tasks. This array consists of a binary heap.
As a feature of the binary heap, the head of the array is always a high-priority task. React uses the sortIndex
property as a priority, and sorts tasks based on the sortIndex
.
A binary heap is an ideal structure for managing tasks because React can pick the highest priority task by simply accessing the head of an array.
2. Add the flushWork
function to the macrotask queue.
After adding the task to a binary heap, React doesn't immediately execute it. React executes the task via the flushWork
function. This function is called through the macrotask queue.
Note that the macrotask queue is not the concept of React. this queue is implemented by JavaScript. JavaScript has two queues, MacroTask and MicroTask. Both have some sort of queue structure, but these roles are completely different.
MacroTask is used to execute an event callback, such as setTimeout
mousemove
, and MicroTask is used to execute Promise
callback. For more information, see javaScript.info.
React uses MacroTask and MicroTask depending on whether the current lane is SyncLine or not. If the current lane is SyncLane
, React uses MicroTask to perform the flushWork
function. Otherwise, React will use MacroTask instead.
This is because JavaScript generally executes the MicroTask before the MacroTask, and SyncLane
is used for an important event like a click event, so React needs to complete the SyncLane
task as quickly as possible for user experience.
In this section, we are looking at the case of non-SyncLane, so React uses the MacroTask.
3. The flushWork
function is called via EventLoop.
Then, the flushWork
function is called via EventLoop. In the flushWork
function, React picks the highest task from the binary heap and executes it (in many cases, this task is to start the rendering phase).
The above process is summarized in the figure below.
Case 2. SyncLane
1. Add the task to the array.
If the current lane is SyncLane
, React adds the task to the array named syncQueue
. This array is not the binary heap structure and doesn't need to be prioritized. This is because this array is only used for SyncLane
tasks, not for tasks from other lanes.
2. Add the flushSyncCallbacks
function to the MicroTask queue.
Then, React queues the function named flushSyncCallbacks
to the MicroTask queue, not the MacroTask queue. This function will execute all of the tasks in the syncQueue
array in a row.
3. The flushSyncCallbacks
function is called via EventLoop.
Then, the flushSyncCallbacks
function is called via EventLoop, and all of the tasks in the syncQueue
array are executed.
The above flow is shown in the figure below.
How does React updates the components?
In the previous sections, I have explained the rendering phase, commit phases, lanes, and scheduling. In this section, I will describe how React updates the components.
I use the following sample code to describe the update flow. This sample code has hooks because it is required to fire any events.
When you hover over the button, a browser will change the text from 'Helloworld' to 'ABCworld'.
<html>
<body>
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
<div id="root"><!-- React will inject elements here --></div>
<script>
const App = () => {
const [test, setTest] = React.useState('')
const onMouseMove = () => {
setTest((test) => 'A')
setTest((test) => test + 'B')
setTest((test) => test + 'C')
}
const child = (test === '') ?
React.createElement('span', {}, 'Hello') :
React.createElement('b', {}, test)
return React.createElement('div', { onMouseMove },
child,
React.createElement('span', {}, 'world')
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(App, {}, null));
</script>
</body>
</html>
Let's look at the update flow after the initial render. The following fiber tree has already been created in the initial render.
What happens when you move the mouse over the <div>
?
1. Create a circular list of hooks.
When you hover the <div>
, the onMouseMove
function in the sample code is called.
const onMouseMove = () => {
setTest((test) => 'A')
setTest((test) => test + 'B')
setTest((test) => test + 'C')
}
The setTest
function is the return value of the useState
function and is used to update the state. So let's see what the setTest
function does inside.
When setTest
function is called, it calls the dispatchSetState
function, and then, it creates the object named update
.
var update = {
lane: lane, // This property holds the lane. In the case of the sample code, the lane is `InputContinuousLane'.
action: action, // This property holds the `onMouseMove` function.
hasEagerState: false, // It is used to improve performance.
eagerState: null, // It is used to improve performance.
next: null // The property has a reference to the next `update` object.
};
```
These `update` objects consist of a circular list. React adds an `update` object to the end of the circular list each time `setTest` is called. In the example, React eventually makes the circular list look like this.
![The circular list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/brlea6sqrnkfoeyasga0.jpg)
Then, React sets this circular list to the `memoizedState` of the Fiber that owns this hook. Also, React doesn't calculate the result (`ABC`) at this point.
![React sets this circular list to the `memoizedState`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f44p7qhn2alvwnj3pjda.jpg)
### 2. The rendering phase is called via EventLoop.
Then, as the previous section described, React pushes the task to the MacroTask queue to start the render phase. After that, EventLoop invokes the task and starts the render phase.
### 3. Copy and reuse the previous Fiber tree.
The difference from the initial rendering is that React already has a Fiber tree. React reuses the previous tree as much as possible instead of creating a new one.
So, where does React have to change? In the example code case, React has to change the `<div><span>Hello</span><span>world</span></div>` element to `<div><b>ABC</b><span>world</span></div>`.
It means, React needs to do the following:
1. Remove the `<span>Hello</span>` element.
2. Add the `<b>ABC</b>` element.
In the React world, we call this process [Reconciliation](https://en.reactjs.org/docs/reconciliation.html).
However, React cannot change the real DOM at the render phase because the render phase could be paused for some reason. If React changes the DOM and the render phase is paused, the user will see the website which may be incomplete.
So, React stores the data in the Fiber instead, and updates the DOM during the commit phase.
This is the time to use a technique called "Double Buffering". React copies the previous Fiber tree instead of creating it from the scratch. In the figure below, the Fiber tree that is currently being worked on is labeled "WIP (work in progress)", and the Fiber tree that is currently displayed is labeled as "Current".
![Copy Fibers to WIP](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7pygs63z22t8neniarhy.jpg)
Note that JavaScript's "copy" basically means "shallow copy". React copies a Fiber, but not its property. The properties of Fiber refer to the same value as the original Fiber. For example, in the figure below, `stateNode` and `memoizedState` are relevant (The `memoizedState` is omitted due to space limitations.)
![Share same property](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/axlkhstjaotdh5055g4w.jpg)
Also, React cannot update the `stateNodes` (DOM) at this point because all `stateNodes` are already appended to `div#root` via the root element and any update of `stateNodes` will be rendered immediately on the screen.
### 4. Appends new Fibers
Then, React executes the circular list that is in `memoizedState` property, and gains a new state, `ABC`.
React then creates a new Fiber that has the `<b>ABC</B>` element, and appends it to the WIP tree.
And React can create the `stateNode` for the new Fiber at this point. This is not a problem because this Fiber is new and is not appended to `div#root`, so a browser will not render this `stateNode`.
![Appends new Fibers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p2u5ypdyn4rew21tgt7h.jpg)
### 5. Copy unchanged Fibers
As same as `3.` step, React copies the unchanged Fibers. In the below figure, React copies the `<span>world</span>` element to WIP.
![Copy unchanged Fibers](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rnlng2b7hc191ojrk55v.jpg)
### 6. Mark the Fiber to be deleted.
React marks Fibers that should be deleted during the commit phase. In this example, it's `<span>Hello</span>`. React stores a reference to this fiber in a parent property named `deletion`, an array.
![Mark the Fiber to be deleted](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hlawzq2pi87xhc95ycyb.jpg)
This is the end of the render phase. In the next section, we will look at the commit phase.
### 7. Update and delete DOM elements in the commit phase
In the commit phase, React updates and deletes elements in the DOM tree based on the Fiber tree and the `deletion` array. First, React deletes the `<span>Hello</span>` element with [removeChild](https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild) from `stateNode` (written in red in the figure) of the parent Fiber.
![Delete an element](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zmjt1wkraofe9gdvl2r8.jpg)
Then, React inserts a new DOM element, `<b>ABC</b>`, into the `stateNode` of the parent Fiber by using [insertBefore](https://developer.mozilla.org/en/docs/Web/API/Node/insertBefore).
![Insert a new element](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1ktwpc39zvmcqgbd1vpy.jpg)
The user will then see the new text "ABCworld" on the screen.
### 8. Makes the WIP tree the current tree.
For the next update, React sets the WIP tree to the `current` property of the FiberRootNode as the current tree. The old "current tree" will no longer be used.
![Makes the WIP tree the current tree.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6f2xr9yz274tchs6tiyy.jpg)
That's all. The rendering is now complete.
## Conclusion
React works with complex algorithms and structures and these are hard to understand. but, according to [principles](https://en.reactjs.org/docs/design-principles.html#implementation), React prioritizes performance and a good developer experience instead of being elegant. So I think this is why most developers love React and prefer to use it.
Top comments (2)
Many error words
bro, you learn the React just read the source code?