React
enabled a brand new architecture, codenamed Fiber, starting from v16. It greatly improves performance compared to previous implementations. This article will analyze the internal architecture of Fiber
in conjunction with an example.
Overview#
Let's start with an example:
class ClickCounter extends React.Component {
constructor (props) {
super(props)
this.state = { count: 0 }
this.handleClick = this.handleClick.bind(this)
}
handleClick () {
this.setState(state => {
return {
count: state.count++
}
})
}
render () {
return [
<button key="1" onClick={this.handleClick}>Update counter</button>,
<span key="2">{this.state.count}</span>
]
}
}
After we click the Update counter button, React
will do the following:
- Update the
count
property of theClickCounter
component'sstate
. - Retrieve and compare the child components of the
ClickCounter
component and theirprops
. - Update the
props
of thespan
element.
Now let's delve into this process ~
From React Elements to Fiber Nodes#
The UI rendered by each React
component is done through the render
method. The JSX
syntax we use is compiled into calls to the React.createElement
method. For example, the above UI structure:
<button key="1" onClick={this.onClick}>Update counter</button>
<span key="2">{this.state.count}</span>
is transformed into the following code:
React.createElement(
'button',
{
key: '1',
onClick: this.onClick
},
'Update counter'
),
React.createElement(
'span',
{
key: '2'
},
this.state.count
)
This will produce the following two data structures:
[
{
$$typeof: Symbol(react.element),
type: 'button',
key: "1",
props: {
children: 'Update counter',
onClick: () => { ... }
}
},
{
$$typeof: Symbol(react.element),
type: 'span',
key: "2",
props: {
children: 0
}
}
]
A brief explanation of the above properties:
$$typeof
: Uniquely identifies as aReact
element.type
,key
,props
describe the properties corresponding to the element.- Other properties like
ref
are not discussed here.
The ClickCounter
component has no props
or key
:
{
$$typeof: Symbol(react.element),
key: null,
props: {},
ref: null,
type: ClickCounter
}
Fiber Nodes#
During the coordination algorithm call, each render
transformed React
element will be merged into the Fiber
node tree. Each React
element has a corresponding Fiber
node. Unlike React
elements, fibers
are not recreated with each render
; they are mutable data structures.
Different types of
React
elements have corresponding types to define the work that needs to be done.From this perspective,
Fiber
can be understood as a data structure that shows what work needs to be done. TheFiber
architecture also provides a convenient way to track, schedule, pause, and stop work.
Once a Fiber
node is created for the first time, subsequent updates will reuse the Fiber
node and only update the necessary properties. If a key
is defined, React
will also choose whether to simply move the node to optimize performance.
After the transformation, we have the following tree structure:
All Fiber
nodes are connected through a linked list and have three properties: child
, sibling
, and return
. (The design of Fiber
nodes will be explained in detail later).
Current and workInProgress Trees#
The tree after transformation is the current tree. When React
starts updating, it traverses the current tree while creating corresponding nodes to form the workInProgress tree. Once all updates and related operations are completed, React
will render the contents of the workInProgress tree to the screen, and then the workInProgress tree becomes the current tree.
The
workInProgress
tree is also referred to as thefinishedWork
tree.
A key function in the source code is:
function updateHostComponent (current, workInProgress, renderExpirationTime) { ... }
Side Effect Linear List#
In addition to regular update operations, React
also defines "side effect" operations: fetching data, subscription operations, or manually changing the DOM structure. These operations are encoded in the Fiber
nodes as the effectTag
field.
ReactSideEffectTags.js defines the operations that will be performed after instance updates:
- For DOM components: Includes adding, updating, or removing elements.
- For class components: Includes updating refs and calling
componentDidMount
,componentDidUpdate
lifecycle methods. - ......
To quickly handle updates, React
employs many efficient measures, one of which is establishing a linear list of Fiber
nodes. Traversing the linear list is faster than traversing the tree. The purpose of establishing the linear list is to mark nodes with DOM updates or other side effects. It is a subset of the finishedWork
tree, connected through the nextEffect
property.
For example: When our update causes c2
to be inserted into the DOM, while d2
and c1
change their property values, and b2
calls a lifecycle method, the side effect linear list will link them together so that React
can directly skip other nodes later:
Next, let's delve into the algorithm executed by Fiber
~
Two Phases#
React
performs work in two main phases: render and commit.
In the render
phase, React
applies updates to components planned through setState
and React.render
and identifies the parts that need to be updated on the UI. If it is the initial render, React
will create a new Fiber
node, and in subsequent updates, it will reuse existing Fiber
nodes for updates. This phase ultimately produces a tree of Fiber
nodes marked with side effects. The side effects describe what needs to be done in the upcoming commit
phase.
The work of the render
phase can be processed asynchronously. During this phase, React
can handle one or more Fiber
nodes based on (browser) idle time, then pause to cache completed work and respond to some events, and can continue processing from where it paused or discard completed parts and start over as needed. This can happen because processing during this phase does not lead to any user-visible changes, such as DOM updates. In simple terms, this phase is interruptible.
During the render
phase, some lifecycle methods are called:
- [Unsafe] componentWillMount (deprecated)
- [Unsafe] componentWillReceiveProps (deprecated)
- getDerivedStateFromProps
- shouldComponentUpdate
- [Unsafe] componentWillUpdate (deprecated)
- render
Starting from v16.3, some legacy lifecycle methods have been marked as unsafe, meaning they are methods that are not recommended for use by the official documentation. They will trigger warnings in future v16.x versions and will be removed in version 17.0. For more details, see Update on Async Rendering.
Why did the official mark them as unsafe? Because the render
phase does not produce side effects like DOM updates, React
can update components asynchronously, but these unsafe methods are often misused by developers, leading to side-effect code being placed in these methods, causing asynchronous rendering errors.
In contrast, the commit
phase is always synchronous because the processing in this phase leads to user-visible changes, such as DOM updates, so React
needs to complete them in one go.
During the commit
phase, these lifecycle methods are called:
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
Since these methods are executed in the synchronous commit
phase, they can contain side-effect code.
Render Phase#
The coordination algorithm always uses the renderRoot function starting from the top HostRoot
fiber node, but it will skip already processed fiber nodes until it encounters a node with unfinished work. For example, if you call setState
deep in the component tree, React
will quickly skip the parent components from the top until it reaches the component that called the setState
method.
Main Steps of the Work Loop#
All fiber nodes are processed in the work loop function. Let's take a look at the implementation of the synchronous part of the loop:
function workLoop (isYieldy) {
if (!isYieldy) {
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
} else {
...
}
}
nextUnitOfWork
contains a reference to the node from the workInProgress
tree that needs to be processed. When React
traverses the Fiber
tree, it uses this variable to know if there are other unfinished fiber nodes. After processing the current fiber node, this variable will point to the next fiber node in the tree or null
. At this point, React
will exit the work loop and prepare to commit changes.
When traversing the Fiber
tree, four main functions are called to initialize or complete work:
Here is a visual animation showing how they are used, with child nodes being prioritized for completion.
First, let's look at the first two functions performUnitOfWork
and beginWork
:
function performUnitOfWork (workInProgress) {
let next = beginWork(workInProgress)
if (next === null) {
next = completeUnitOfWork(workInProgress)
}
return next
}
function beginWork (workInProgress) {
console.log('work performed for ' + workInProgress.name)
return workInProgress.child
}
The performUnitOfWork
function receives a fiber node from the workInProgress
tree and starts work by calling the beginWork
function. This function performs all necessary operations for a fiber node (simplified here by just printing the fiber node's name to indicate completion). The beginWork
function always returns a pointer to the next child node or null
.
If there is a next child node, it will be assigned to the variable nextUnitOfWork
in the workLoop
function. If not, React
knows it has reached the end of this node branch and can complete the current node. Once a node is completed, it will process the work of sibling nodes and then backtrack to the parent node. Let's look at the completeUnitOfWork
function:
function completeUnitOfWork (workInProgress) {
while (true) {
let returnFiber = workInProgress.return
let siblingFiber = workInProgress.sibling
nextUnitOfWork = completeWork(workInProgress)
if (siblingFiber !== null) {
return siblingFiber
} else if (returnFiber !== null) {
workInProgress = returnFiber
continue
} else {
return null
}
}
}
function completeWork (workInProgress) {
console.log('work completed for ' + workInProgress.name)
return null
}
Commit Phase#
This phase starts with the call to the completeRoot function. Here, React
updates the DOM and calls mutable lifecycle methods.
In this phase, React
has the current
and finishedWork
(workInProgress
) trees, as well as the side effect linear list.
The side effect linear list tells us which nodes need to be inserted, updated, or deleted, or which components need to call their lifecycle methods. This is what the entire commit phase will traverse and process.
The main function running in the commit phase is commitRoot, which does the following:
- Calls the
getSnapshotBeforeUpdate
method of nodes marked with theSnapshot
side effect. - Calls the
componentWillUnmount
method of nodes marked with theDeletion
side effect. - Executes all DOM insertion, updating, and deletion operations.
- Sets the
finishedWork
tree ascurrent
. - Calls the
componentDidMount
method of nodes marked with thePlacement
side effect. - Calls the
componentDidUpdate
method of nodes marked with theUpdate
side effect.
Here is a simplified version of the function describing the above process:
function commitRoot (root, finishedWork) {
commitBeforeMutationLifecycles()
commitAllHostEffects()
root.current = finishedWork
commitAllLifeCycles()
}
Each sub-function implements a loop to traverse the side effect linear list and check the type of side effect to update:
-
commitBeforeMutationLifecycles
: Traverses and checks if nodes have theSnapshot
side effect mark.function commitBeforeMutationLifecycles () { while (nextEffect !== null) { const effectTag = nextEffect.effectTag if (effectTag & Snapshot) { const current = nextEffect.alternate commitBeforeMutationLifeCycles(current, nextEffect) } nextEffect = nextEffect.nextEffect } }
-
commitAllHostEffects
: The place where DOM updates are executed, defining the operations that need to be performed on the nodes and executing them.function commitAllHostEffects () { switch (primaryEffectTag) { case Placement: { commitPlacement(nextEffect) ... } case PlacementAndUpdate: { commitPlacement(nextEffect) commitWork(current, nextEffect) ... } case Update: { commitWork(current, nextEffect) ... } case Deletion: { commitDeletion(nextEffect) ... } } }
Interestingly, in the
commitDeletion
function,React
calls thecomponentWillUnmount
method. -
commitAllLifecycles
:React
calls the remaining lifecycle methodscomponentDidMount
andcomponentDidUpdate
.
Now that we understand the main execution process of the coordination algorithm, let's focus on the design of Fiber
nodes: they are all connected through a linked list and have three properties: child
, sibling
, and return
. Why is this design chosen?
Design Principles#
We already know that the Fiber
architecture has two main phases: render and commit.
In the render phase, React
traverses the entire component tree and performs a series of operations, all of which are executed internally in Fiber
. Different element types have different work to handle. As Andrew said:
When dealing with UI, a significant issue is that if a large amount of work is executed simultaneously, it can cause frame drops in animations.
If React
synchronously traverses the entire component tree and processes work for each component, it can easily run over 16ms, leading to visual stuttering. However, there is no need to adopt a synchronous approach. Some key points in React
's design principles are:
- Not all UI updates need to take effect immediately (which may cause frame drops).
- Different types of updates have different priorities (animation responses are much higher than data fetching).
- A push-based scheme is determined by the developer on how to schedule; while a pull-based scheme is determined by
React
on how to schedule.
Based on these principles, the architecture we need to implement should:
- Pause tasks and be able to resume later.
- Set different priorities for different tasks.
- Reuse already completed tasks.
- Terminate tasks that are no longer needed.
So what can help us achieve this?
Modern browsers (and React Native) have implemented APIs that help solve this problem: requestIdleCallback.
This global function can be used to queue functions to be called during the browser's idle time. Here's a simple look at its usage:
requestIdleCallback((deadline) => {
console.log(deadline.timeRemaining(), deadline.didTimeout)
})
deadline.timeRemaining()
shows how much time is available to do any work, and deadline.didTimeout
indicates whether all allocated time has been used up. timeRemaining
changes immediately after the browser completes certain tasks, so it must be checked continuously.
requestIdleCallback
is actually a bit too strict, often leading to insufficient smooth UI rendering, so the React team had to reimplement their own version.
React
calls requestIdleCallback
to schedule work, placing everything to be executed into the performWork
function:
requestIdleCallback((deadline) => {
while ((deadlne.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
nextComponent = performWork(nextComponent)
}
})
To make good use of the API for processing work, we need to break down rendering work into multiple incremental units. To solve this problem, React
reimplemented the algorithm, shifting from a synchronous recursive model relying on the built-in stack to an asynchronous model with linked lists and pointers. As Andrew wrote:
If we only rely on the built-in stack, it will keep working until the stack is empty. Wouldn't it be better if we could freely interrupt the stack and manually manipulate stack frames? This is the purpose of React Fiber. Fiber is a stack specifically reimplemented for React components, and you can think of a single fiber as a virtual stack frame.
Recursive Traversal#
React's official documentation describes the previous recursive process:
By default, when recursively iterating over the child nodes of a DOM node,
React
will iterate over both child lists simultaneously and generate a change when differences occur.
Each recursive call adds a frame to the stack, which is executed synchronously. We would have a component tree like this:
The recursive approach is very intuitive, but as we mentioned, it has limitations. The biggest issue is that it cannot split tasks and cannot flexibly control tasks. Therefore, React
proposed a new linked list tree traversal algorithm, making it possible to pause traversal and terminate the growing stack.
Linked List Traversal#
Sebastian Markbage describes this algorithm here. To implement this algorithm, we need a data structure with these three properties:
- child: Points to the first child node
- sibling: Points to the first sibling node
- return: Points to the parent node
Based on this data structure, we have such a component structure:
With this structure, we can replace the browser's stack implementation with our own.
If you want to see the detailed code implementations of the two traversals, you can refer to The how and why on React’s usage of linked list in Fiber to walk the component’s tree.
Finally, let's take a look at the detailed structure of a Fiber
node, using ClickCounter
and span
as examples:
{
stateNode: new ClickCounter,
type: ClickCounter,
alternate: null,
key: null,
updateQueue: null,
memoizedState: { count: 0 },
pendingProps: {},
memoizedProps: {},
tag: 1,
effectTag: 0,
nextEffect: null
}
{
stateNode: new HTMLSpanElement,
type: "span",
alternate: null,
key: "2",
updateQueue: null,
memoizedState: null,
pendingProps: {children: 0},
memoizedProps: {children: 0},
tag: 5,
effectTag: 0,
nextEffect: null
}
alternate
, effectTag
, and nextEffect
have been explained earlier. Let's look at the other properties:
- stateNode: Refers to class components, DOM nodes, or other
React
element types related to the fiber node. - type: Describes the component corresponding to this fiber.
- tag: Defines the type of fiber, determining what work needs to be processed. (The processing function is createFiberFromTypeAndProps)
- updateQueue: A queue of state updates, callback functions, and DOM updates.
- memoizedState: Used to create the output state of the fiber. When updating, it reflects the state currently rendered on the screen.
- pendingProps: In contrast to
memoizedProps
, the former is set when execution begins, while the latter is set when it ends. If the incomingpendingProps
equalsmemoizedProps
, it indicates that this fiber's previous output can be reused, avoiding unnecessary work. - key: As a unique identifier, it helps
React
determine which items have changed, been added, or removed. (See lists and keys)
There is also a video worth watching about the operation of Fiber
: Lin Clark - A Cartoon Intro to Fiber - React Conf 2017.
References: