The reconciler is the core part of React. It figures out the changes that has occurred between the previous state of your app and the next state. This process of figuring it out is known as reconciliation.
This is different from the actual manipulation of the DOM, which is done by a renderer. Renderers are pluggable but there’s only one reconciler and it comes with React itself.
Examples of renderers are React DOM, React Native, React PDF, React Hardware, React Three Renderer, etc…
The current React reconciler is the Fiber Reconciler (since React 16) and its named after Fiber which is just a plain JavaScript object. It solves issues of the previous reconciler and introduces new features to React.
What and why
In a given time frame, there’re many things happening in a React app. These things can be categorized into low and high priority tasks. When you’ve high priority tasks with low priority tasks and browser’s own tasks then your high priority tasks could end up behind other tasks.
This is because JavaScript is single threaded, and at a given time it’ll be executing a single task on the main thread.
High priority task example: typing in a text field. Low priority task example: rendering elements based on server response. Browser tasks example: css animation, browser resizes, network calls, etc…
Fiber reconciler is optimized to work efficiently with the main thread. It does this by splitting up work and prioritizing them. It can pause work and come back to it later on. It can use previously completed work or completly abort a partially done work.
This makes Fiber reconciler asynchronous in nature as a oppose to the synchronous Stack reconciler.
Fiber focuses on animations and responsiveness.
Stack Reconciler
Initially create elements (returned by the render
method) for describing the UI. It then creates component instances for figuring out the previous state, next state, and changes it needs to make. At last, we’ve the DOM nodes that represents the UI.
In the stack reconciler these steps happen layer by layer. So it’ll create an element, then it’ll update/create an instance, and then will update the DOM node. To do this it recursively calls mount and update lifecycle methods until it gets to the end of the tree.
As you can see, the main thread is getting stuck due these calls. In order to know if there’s any other work that needs to be done, we’ll have to break this up. React Fiber does exactly that.
It makes it possible to compute a little part of the tree and then check if there’s any other work to do. The way it keeps track of where its in the tree is through the Fiber data structure.
Work loop is the reason for this graph (more on this in “Render/Reconciliation Phase” section).
The stack reconciler used a stack to keep a track of where its in the tree. Work was pushed and popped of this stack. React had to work until the stack was empty. This made it synchronous and hence it can’t be interrupted.
This synchronicity caused a lot of problems.
For example, you’ve a text field and you would obviously want to type in it without any delay. If there’s a network request happening in the background that results into some elements begin render. Now, if we type in the text field while those elements are begin rendered, we’ll experience delay because the reconciler is in between processing those elements.
Fiber not only makes React more performant but also makes it “smarter” as it becomes more aware of changes happening, gives flexibility to make or defer changes, etc…
Fiber
Fiber keeps track of where we’re in the tree during the reconciliation phase. Its a plain JavaScript object with properties.
It has one-to-one relationship with the instance (stateNode
property on Fiber) and manages its work for that instance. A fiber also keeps track of its relationships with other fibers in the trees using properties on it.
The first fiber in the tree is the Host Root and that’s the container in which the React app is injected i.e. <div id="app"></div>
. The following diagram represent the relationship between fibers.
div
has parent relationship with the child section
and the child has return relationship with the parent. Similar parent-child relationship is between section
its first child i.e. h1
, but the other children has sibling relationship with the first child and aren’t directly connected with the their parent. React will traverse this layer of the three via these siblings.
So a fiber ends up with structure like this:
{
stateNode
child
return
sibling
...
}
Reconciliation with Fiber
Let’s consider the following example for understanding the entire reconciliaiton and rendering with Fiber reconciler:
import { useState } from "react";
export default function App(): JSX.Element {
var [nums, setNums] = useState([1, 2, 3]);
return (
<List>
<button onClick={() => raisedToPowerOf2()}>Click</button>
<Item text={nums[0]} />
<Item text={nums[1]} />
<Item text={nums[2]} />
</List>
);
function raisedToPowerOf2() {
setNums(nums.map((n) => n * n));
}
}
function List({ children }: { children: JSX.Element[] }) {
return <div>{children}</div>;
}
function Item({ text }: { text: number }) {
return <div>{text}</div>;
}
React creates a fiber tree in the initial render and its named as current fiber tree. Along with that React also initiates another fiber tree named as work in progress tree.
The reason for having the work in progress tree is that while computing changes (the diffing), we don’t want to simultaneously make changes in the DOM. This differs from how the stack reconciler works. There, as its walking down the tree and changing the instances, its also changing the corresponding DOM nodes. For example,
This is fine if you’re doing everything simultaneously i.e. synchronously, but if you want to interrupt between 2nd and 3rd item updates then the browser will go on doing other tasks and this will make the UI inconsistent.
To avoid this problem Fiber introduces 2 phases:
- The Render/Reconciliation phase where it’ll build up the work in progress fiber tree and computes all the changes that it need to makes.
- In the Commit phase it’ll makes those changes in the DOM.
The Reconciliation phase is interruptible. React can allow browser to do other work at times in the reconciliation phase. The commit phase can’t be interrupted otherwise you’ll get UI inconsistencies.
Fiber represent a unit of work. React processes fibers and we end up with “finished work”. This work is later on committed, resulting in visible changes in the DOM.
Render/Reconciliation Phase
We’ll have the current
and workInProgress
fiber trees.
Let’s say we’ve called setState
. React will add this to a update queue (list of all the updates) on the List
and then React will schedule work for this. This work doesn’t needs to happen immediately, so React defers this work by calling requestIdleCallback
. When the main thread has spare time it’ll do this work.
So when the main thread has some spare time, it’ll comes to React and React will start processing. This time could be few milliseconds or update 50 milliseconds in case there’re no frames scheduled in the new future.
Once the main thread comes back for React, we start building up the workInProgress
tree and we’ll fill it in with a function called workLoop
. The workLoop
is what gives Fiber a wave like call stack trace. This is because it allows React to do some work and then allow the main thread to do something and then repeat. In order to do this it needs to keep track of 2 things:
- the next unit of work it needs to do,
- time its has remaining with the main thread
HostRoot
We’ll start with the HostRoot
. It’ll copy the current version of HostRoot
from current fiber tree to the workInProgress
tree and it’ll have pointer to HostRoot
's child i.e. the List
.
Since there’re no updates in the HostRoot
, it’ll clone List
into the workInProgress
tree and since the List
has an update queue, it’ll also clone that update queue too. Now the List
fiber will be returned as the next unit of work.
Now React will check whether its time with the main thread has expired or not. Let’s say we’ve some time pending. So React will continue its work.
List
React will process the update queue of List
. React will update, get the new state, and the update queue’s processing is done. List
will be marked as its has changes that needs to be committed in the DOM.
In order to go down the tree we’ll pass the props and set the states to generate List’s children. React goes through the array of elements and check whether fiber from the current for these elements can be reused. If yes then it’ll clone them to the workInProgress
tree. Finally it returns the button as the next unit of work.
Let’s say we resized the window. This doesn’t have anything to do with React itself. This’ll add work in requestIdleCallback
which the main thread has to take care of, but not Immediately as it has sometime with React and will continue processing React’s work.
Button
Now we’re at button. Since button doesn’t have any other children, React will complete button’s work i.e. completing old and new to check if there’re any changes or not and if there’re any changes then it’ll marks button for committing these changes in the DOM.
Now move to Item
i.e. sibling of button
will be returned as next unit of work. If there’s shouldComponentUpdate
on this Item
and next props and next states passed check whether it should update or not. If false
it won’t mark this item as changed. Then it returns its sibling i.e. Item
as next unit of work.
The second Item
has shouldComponentUpdate
and it returns true
and the Item
is marked as changed. The div
(its child is cloned) and is returned as next unit of work.
We check the timer and there’s some time left. So we continue processing the div
work. The div
doesn’t have any children so we complete its work. We compare the old with new and see that the text in the div
has changed, so we mark this div
as changed and it’s also added to the list of changes in its parent i.e. Item
2. Item
will keep track of all the fibers underneath it that has changes.
Fiber keeps track of all of the fibers underneath it that changes.
Now the div
doesn’t has any child, it’ll call complete on its parent. The parent doesn’t has any other work to process.
This is the first time we’ve added anything to the list of changes. The div
is added to the list changes because it was marked as changed and its work was done. Also the Item
2’s work is completed and it also is marked has changed. So now it’ll move up and will merge its list of changes to its parents list of changes. This list of changes are called Effect List.
The Item
takes div
and puts its changes to the effect list and then puts its changes at the end.
Then it returns its sibling as the next unit of work i.e. Item
3.
Now we check the timer and let’s say its over. Now React has to let the main thread to do other work. React still hasn’t finited its job so it uses requestIdleCallback
to basically say to main thread that “When you’re done doing other things, come back to me and we’ll finish the remaining work”.
The main thread continues other work which includes the resizing of the window. Nothing in the context of React app is changing. React know that it needs to change the text sometime in the future but it doesn’t do that now.
When the main thread is done with its work, it comes to React, and they start from where they left off. The last units of work are done the same and get their marks of change, and are added the effects list.
Now all of the work is done below List
, so complete is being called on the List
. This means that List
moves everything up into its parent effect list and adds itself to the end.
Finally the List
is completed, it can the complete the HostRoot
.
Now React set workInProgress
tree as pending commit. This is the end of first phase. We’ve updated the workInProgress
tree and figured out list of changes in that tree. Now its time for phase 2 i.e. committing these changes to the DOM.
React will check if it has time left with the main thread and if yes then it’ll make those commits else it’ll use the requestIdleCallback
and as soon as the main thread returns, the first thing React will do is committing these pending commits.
Commit Phase
React will go through the effect list and make changes to the DOM.
Starting with the first fiber i.e. div
. It makes changes to the DOM and then we go the Item
. If there were any ref
then it’ll be removed now will be attached later on.
Now once the changes are committed to the DOM, the workInProgress
tree becomes the more up to date version of the app. So React changes the pointers of current tree to the work in progress tree and work in progress tree to the current tree.
A benefit of this is that React can use old objects, it can reuse things in the work in progress tree and just copy over key values then next time it has to build up a work in progress tree. This is known as double buffering and it saves time on memory allocation and garbage collection.
Now the commit is done.
React goes through the effect list one more time and does some more work. It will do the rest of the life cycle hook, update any refs and will also handle error boundaries.
Once this is done the commit phase ends.
Fixing the priority issue
Breaking up work makes React more responsive to browser events. This feature in and of itself it doesn’t fixes the problem where important React commit got caught behind other React commits (lower priority commits).
In order to solve this we have to add priorities. In Fiber Reconciler each update needs to have priority assigned to it. Current priorities:
- Synchronous — same as stack reconciler
- Task — before next tick
- Animation — before next frame
- High — scheduled using requestIdleCallback, pretty soon
- Low — scheduled using requestIdleCallback, minor delay ok. Eg data fetching
- Offscreen — scheduled using requestIdleCallback, prep for display/scroll. Eg: something that is hidden or offscreen, and its nice to have render in case it gets shown but no need to have it now
Example of how these priorities work.
Let’s say in the above example, in between we want to have an update urgently. User click on a button, its put in the callback in the main thread queue, but the main thread is not going to take of it util its deadline with React has been reached, and when it is then it’ll take care of that callback. Then React will add this to the list of updates but since its a high priority update, its put ahead of the low priority update, that we were already working on.
This mean that when React starts work again, it doesn’t starts on the next unit of work, its not going to start the previously set unit of work, instead it starts with HostRoot and throws away all of the work that was done. React will go through the high priority update and will commit. After this it’ll start working on the low priority update.
Some questions
Some of the lifecycle hooks are fired in the reconciliation phase and others are fired in the commit phase. This means that lifecycle method won’t fire in the same order that we expect.
+ componentWillUpdate (low priority)
+ componentWillUpdate (high priority)
+ componentDidUpdate (high priority)
+ componentWillUpdate (low priority)
+ componentDidUpdate (low priority)
React team is working on this.
What if a lot of high priority tasks jumps ahead of low priority tasks and those low priority task never ends up happening. This scenario is called starvation. React tries to fix this by reusing the work where it can like if there’s place where low priority work was done and high priority task didn’t touch that part of the tree then it will reuse that work and other such methods to mitigate this issue.
Features
React reconciler makes updates play well together by allowing more high priority updates jump ahead of low priority updates. This makes UI more fluid and responsive. It does this by breaking up the work into smaller units of work. These work could be paused so that React can get to other tasks that it needs to do. This is called cooperative scheduling.
New possibility like lazy loading where for React to build a single tree, it has to fetch of the components in a single file and then it loads and start building up the instances or the fiber tree. This can be different by React building up the tree as component stream (Streaming Rendering) in. Fiber will make this easier and smarter.
Another thing that fiber will make easier is working in parallel. Having workers other than the main thread will lead to work being done quickly. Fiber will make it easier to do parallelizing easier by splitting up branches of the tree and doing branches of the tree in parallel.