React Fiber Reconciler

AkashSDas
15 min readJan 21, 2024

--

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.

React Fiber Reconciler

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.

Execution for different tasks via the main thread

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.

Reconciler working steps — practical example
Reconciler working steps — concept view
Reconciler steps

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.

Stack reconciler call stack trace
Stack reconciler call stack trace

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.

Fiber reconciler call stack trace
Fiber reconciler call stack trace

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.

Fiber Tree

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,

Stack reconciler walking through the tree and updating DOM
Stack reconciler walking through the tree and updating DOM

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.

Work Loop
Work Loop allowing main thread to toggle between browser and React tasks

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

Starting processing with the `HostRoot`
Starting processing with the `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.

Clone `HostRoot`, no updates there, clone `List`, clone `List`’s update queue, return `List` as 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

Processing the next unit of work i.e. List
Processing the next unit of work i.e. `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 is the next unit of work
`Button` is the next unit of 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.

Processing Item2’s 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.

Effect list with div and Item2 changes
Effect list with div and Item2 changes

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.

React’s time with main thread is over
React’s time with main thread is over

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.

Pending commit

Commit Phase

React will go through the effect list and make changes to the DOM.

Going through the effect list and committing changes

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.

Switching current and workInProgress fiber tree pointers

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.

--

--