React Fiber: A Tale of Two Reconcilers

Glimpsing the secrets behind React’s efficient rendering

Sen Lin
The Tech Collective
8 min readJan 9, 2024

--

Image generated by DALL·E 3

So, back in uni, if you studied computer science, chances are you stumbled upon this course called Algorithms. This kind of course was all about data structures like stacks, heaps, linked lists, etc. At that time, these concepts felt somewhat distant to me, especially when I was more into other stuff like web and mobile development.

But as I learned more about React, I realised these algorithms weren’t just academic; they were actually doing some heavy lifting in React. Take Fiber, for example. In a nutshell, Fiber is used for React to render components more efficiently. It incorporates various data structures and algorithms concepts we studied in uni.

Before we discuss Fiber in more detail, we need to dive a little into JavaScript.

Just JS

The most important thing here is JavaScript is single-threaded. This means JS can only do one thing at a time. It’s like driving on a one-lane road (actually, we’ll talk about Lane later) where everyone has to wait their turn.

In the world of web browsers, the main thread is responsible for various things, like parsing HTML to build the DOM, parsing CSS to build the CSS Object Model (CSSOM), and executing JavaScript.

These works are crucial for our smooth web experience. However, this is also where it gets tricky for web apps. If a time-consuming task holds the main thread, other stuff like user interaction can get stuck, leaving the user feeling like the webpage is unresponsive.

What we’re going to discuss is how React handles these constraints in the browser.

React reconciliation

Now, let’s talk React. At its core, React is a user interface library for building dynamic web applications. The important feature here is React knows how to smartly update what you see on the screen in response to data changes.

And this is done through something called reconciliation. Think about React as an observer, always watching for any changes in your components’ state or props. Reconciliation, in simple terms, is React playing the comparison game.

Take a look at another example:

function Counter({ count, title }) {
return(
<div>
<h2>{title}</h2>
<div>Count: {count}</div>
</div>
);
}

Let’s say we want the title to remain the same, but the countchanges. Initially, we have a component <Counter count={0} title="Click Counter" />. After the update we have <Counter count={1} title="Click Counter" /> . React will spot the change in the numbers (0 and 1) and update just that bit, leaving the title part alone.

Why is this a big deal? Imagine if React had to redo the entire webpage every time you made a tiny change. That would be like repainting your entire house just because you hung up a new picture.

But thanks to reconciliation, React only touches what needs changing. This process is the secret behind React’s performance, making it seem like it’s re-rendering everything when you call setState, while it’s actually just being very efficient under the hood.

So, now that we’ve got what and the why, let’s dive into how.

Stack Reconciler

Before React 16, it had the stack reconciler. As we discussed about the reconciliation process, the main job of the reconciler is to look at the UI’s current state&props and compare it with what the state&props looked like after some changes.

If you still remember something in the Algorithm class, the stack reconciler was about depth-first traversal (DFS). The reconciler would traverse from root to the left, checking out each component, and comparing the old props with new props. The important thing about this traversal is that once the process starts, it won’t stop until it is finished.

Sounds straightforward right? But the problem is, imagine the reconciler is walking through a huge component tree, updating as it goes, and it just can’t take a break. In big apps, this could mean the main thread gets stuck for a long time. Not a good thing if you want your web app responsive.

Another problem is that this approach was like treating everything with the same urgency. Whether it was user interaction/animation or just some background data update, they all look the same to the stack reconciler.

Fiber Reconciler

So when React hit version 16, it brought the new Fiber reconciler. It’s the re-implementation of the old stack reconciler. So they’re still doing the same thing — help React spot and handle the difference, but Fiber does it more efficiently.

What’s the big deal with Fiber? Well, it abandoned the stack data structure and went for a linked list instead. This is cool because, unlike the stack reconciler that just blindly processes from start to end, Fiber can actually pause on a task, deal with something more important like user inputs or animations, and then pick up right where it left off.

But what exactly is this Fiber thing? At its heart, it’s just a JavaScript object. It’s like a virtual DOM node, but it’s got some extra properties, like details about the component. Let’s break down some key Fiber properties (you can find them in the ReactInternalTypes.js file in the React source code).

  • type: this is straightforward. It tells what kind of React component you’re dealing with — it could be div, span or your custom component.
  • child, sibling, return: sounds like family members but they are all about linking Fibers together. child points to the first child; sibling to the next one in line; return to the parent
  • pendingProps and memoizedProps: pendingProps are new-coming props waiting to be used in the next render. They called pending because they have not been applied yet. memoizedProps are the props used in the last render. React will make the comparison here to see if a re-render is needed. For example, if you have a Button component and it receives a colour prop. Initially it was red and in the next render cycle, you change it to blue. Now the pendingProps is red and memoizedProps is blue.

What does Fiber bring?

Fiber brings a lot of benefits but I want to discuss two main benefits here, which directly correspond to the limitations of the stack reconciler we discussed above.

Incremental Rendering

Remember the stack reconciler? It would update the entire component tree in one go. Fiber, on the other hand, breaks the rendering job into chunks. This spreads out the work, making the UI updates smoother. This is a significant improvement over the previous stack reconciler.

Priority Levels

Fiber also brings priority concept for updates. It lets React sort tasks by importance. So things users can see and interact with have higher priority while other stuff behind the scenes have lower priority.

I want to talk more about this topic because some articles I found online talk about the pendingWorkPriority property. But actually, React 18 is using the new Lane model, a fancier way to handle priorities with a 32-bit system.

Let’s take a look at these lanes in React:

The smaller the number, the higher the priority (if you’re curious, the prefix 0b means the number follows the binary format, otherwise it would be interpreted as a digital number).

Why use 32 bits to represent the priority instead of a single number (like the previous implementation)? Because this approach is more efficient and flexible.

Specifically, each lane (or bit) stands for a type of task or priority level. Higher-priority tasks have smaller binary values (the rightmost bit in the binary representation). When multiple lanes are presented, their lanes are merged using bitwise OR operation.

After the merge operation, we get a combined lane set. For example, merging lanes 0001 and 0010 results in a lane set of 0011. This happens because the first bit of 0001 is 1, and the first bit of 0010 is 0. Since 1 OR 0 equals 1, the first bit of the resulting lane set is 1. By following this operation rule, we finally have 0010 || 0001 = 0011.

This lane set represents the combined tasks. React processes the tasks on the lane in order from the rightmost (highest priority) to the left. When tasks in a lane are completed, the lane’s bit is turned off in the lane set (ex. from 0011 to 0010). But if new tasks come in during processing, they are merged into the lane set. If higher-priority tasks are added, React can pause the lower-priority tasks and go back to handling the higher-priority task.

For example, if React is processing a background data update(lane 0010) and suddenly the user clicks a button, trigger a high-priority task(0001). React combines these tasks into a lane set (0011). It starts with the high-priority user interaction task(rightmost). Once this task is completed, the lane set updates to 0010, and React resumes the background update task.

Fiber Lifecycle

Finally, a quick word on Fiber’s lifecycle. The most important two phases are the render phase and the commit phase. We‘ve discussed a lot about the render phase. It’s basically the reconciliation phase, comparing the difference between the previous virtual DOM (current tree) and the current DOM (work-in-progress tree), allowing React to pause and resume work. Remember we said that without reconciliation React has to re-build the entire DOM tree every time a change occurs? Now React can just copy the current Fiber tree to the work-in-progress tree and make changes there only if necessary.

After all the planning in the render phase, we hit the commit phase. This is where React makes the virtual real. It’s all about updating the DOM to reflect the changes. The most important thing here is in this phase the updating process cannot be paused. Once React starts updating the DOM it goes all the way until it’s done.

And that wraps up the basic intro to Fiber! Now we’ve seen how Fiber makes React’s rendering smoother and more efficient. As mentioned at the beginning, this shows how concepts from data structures and algorithms can really make a difference in the world of web development.

I wrote this article to share what I’ve learned from studying React’s code. I hope you find it helpful as well. A lot of what I’ve learned came from helpful articles and resources by other authors. A massive thanks to those authors. Be sure to check them out if you’re interested in more details.

References:

--

--