Karthik Kalyanaraman Software engineer. Curious about technology and the economics of the tech industry. Follow me on Twitter @karthikkalyan90.

A deep dive into React Fiber

14 min read 3968

Deep Dive React Fiber

Editor’s note: This post was updated on 14 March 2022 to remove any outdated information and add the What is React Fiber? section.

Ever wondered what happens when you call ReactDOM.render(<App />, document.getElementById('root'))?

We know that ReactDOM builds the DOM tree under the hood and renders the application on the screen. But how does React actually build the DOM tree? And how does it update the tree when the app’s state changes?

In this post, we’ll learn what React Fiber is and how React built the DOM tree until React v15.0.0, the pitfalls of that model, and how the new model from React v16.0.0 to the current version solves these problems.

This post will cover a wide range of concepts that are purely internal implementation details and are not strictly necessary for actual frontend development using React. With that said, we’ll cover:

What is React Fiber?

React Fiber is an internal engine change geared to make React faster and smarter. The Fiber reconciler, which became the default reconciler for React 16 and above, is a complete rewrite of React’s reconciliation algorithm to solve some long-standing issues in React.

Because Fiber is asynchronous, React can:

  • Pause, resume, and restart rendering work on components as new updates come in
  • Reuse previously completed work and even abort it if not needed
  • Split work into chunks and prioritize tasks based on importance

This change allows React to break away from the limits of the synchronous stack reconciler. Previously, you could add or remove items, for example, but it had to work until the stack was empty, and tasks couldn’t be interrupted.

This change also allows React to fine-tune rendering components, ensuring that the most important updates happen as soon as possible.

Now, to truly understand the powers of Fiber, let’s talk about the old reconciler: the stack reconciler.

We made a custom demo for .
No really. Click here to check it out.

React’s stack reconciler

Let’s start with our familiar ReactDOM.render(<App />, document.getElementById('root')).

The ReactDOM module passes the <App/ > to the reconciler, but there are two questions here:

  1. What does <App /> refer to?
  2. What is the reconciler?

Let’s unpack these two questions.

What is <App />?

<App /> is a React element and “elements describe the tree.” According to the React blog, “An element is a plain object describing a component instance or DOM node and its desired properties.”

In other words, elements are not actual DOM nodes or component instances; they are a way to describe to React what kind of elements they are, what properties they hold, and who their children are.

This is where React’s real power lies: React abstracts away the complex pieces of how to build, render, and manage the lifecycle of the actual DOM tree by itself, effectively making the life of the developer easier.

To understand what this really means, let’s look at a traditional approach using object-oriented concepts.

Object-oriented programing in React

In the typical object-oriented programming world, developers must instantiate and manage the lifecycle of every DOM element. For instance, if you want to create a simple form and a submit button, the state management still requires some effort from the developer.

Let’s assume the Button component has a isSubmitted state variable. The lifecycle of the Button component looks something like the flowchart below, where each state must be managed by the app:

Button Component Lifecycle Flowchart

This size of the flowchart and the number of lines of code grow exponentially as the number of state variables increase.

So, React has elements to solve this problem; in React, there are two kinds of elements: the DOM element and the component element.

The DOM element is an element that’s a string; for instance, <button class="okButton"> OK </button>.

The component element is a class or a function, for example, <Button className="okButton"> OK </Button>, where <Button> is either a class or a functional component. These are the typical React components we generally use.

It is important to understand that both types are simple objects. They are mere descriptions of what must be rendered on the screen and don’t instigate rendering when you create and instantiate them.

What is React reconciliation?

This makes it easier for React to parse and traverse them to build the DOM tree. The actual rendering happens later when traversing finishes.

When React encounters a class or a function component, it will ask that element what element it renders to base on its props.

For instance, if the <App> component rendered the the following, then React will ask the <Form> and <Button> components what they render to based on their corresponding props:

<Form>
  <Button>
    Submit
  </Button>
</Form>

So, if the Form component is a functional component that looks like the following, React will call render() to know what elements it renders and see that it renders a <div> with a child

const Form = (props) => {
  return(
    <div className="form">
      {props.form}
    </div>
  )
}

React will repeat this process until it knows the underlying DOM tag elements for every component on the page.

This exact process of recursively traversing a tree to know the underlying DOM tag elements of a React app’s component tree is known as reconciliation.

By the end of the reconciliation, React knows the result of the DOM tree, and a renderer like react-dom or react-native applies the minimal set of changes necessary to update the DOM nodes.This means that when you call ReactDOM.render() or setState(), React performs reconciliation.

In the case of setState, it performs a traversal and determines what changed in the tree by diffing the new tree with the rendered tree. Then, it applies those changes to the current tree, thereby updating the state corresponding to the setState() call.

Now that we understand what reconciliation is, let’s look at the pitfalls of this model.

What is the React stack reconciler?

Oh, by the way, why is this called the “stack” reconciler? This name is derived from the “stack” data structure, which is a last-in, first-out mechanism.

And, what does stack have anything to do with what we just saw? Well, as it turns out, since we are effectively performing recursion, it has everything to do with a stack.

What is recursion in React?

To understand why that’s the case, let’s take a simple example and see what happens in the call stack:

function fib(n) {
  if (n < 2){
    return n
  }
  return fib(n - 1) + fib (n - 2)
}

fib(10)

As we can see, the call stack pushes every call to fib() into the stack until it pops fib(1), which is the first function call to return.

Then, it continues pushing the recursive calls and pops again when it reaches the return statement. In this way, it effectively uses the call stack until fib(3) returns and becomes the last item pop from the stack.

Call Stack Diagram

The reconciliation algorithm we just saw is a purely recursive algorithm. An update results in the entire subtree rerendering immediately. While this works well, this has some limitations.

As Andrew Clark notes, in a UI, it’s not necessary for every update to apply immediately; in fact, doing so can be wasteful, causing frames to drop and degrading the user experience.

Also, different types of updates have different priorities—an animation update must complete faster than an update from a data store.

Problems with dropped frames

Now, what do we mean when we refer to dropped frames, and why is this a problem with the recursive approach? To understand this, let’s briefly review what frame rate is and why it’s important from a user experience point of view.

What is frame rate?

Frame rate is the frequency consecutive images appear on a display. Everything we see on our computer screens are composed of images or frames played on the screen at a rate that appears instantaneous to the eye.

To understand what this means, think of the computer display as a flip-book and the pages of the flip-book as frames played at some rate when you flip them.

Comparatively, a computer display is nothing but an automatic flip-book that plays continuously when things change on the screen.

Typically, for a video to feel smooth and instantaneous to the human eye, the video must play at a rate of about 30 frames per second (FPS); anything higher gives a better experience.

Most devices these days refresh their screens at 60 FPS, 1/60 = 16.67ms, which means a new frame displays every 16ms. This number is important because if the React renderer takes more than 16ms to render something on the screen, the browser drops that frame.

In reality, however, the browser has housekeeping to do, so all your work must complete inside 10ms. When you fail to meet this budget, the frame rate drops, and the content judders on screen. This is often referred to as jank, and it negatively impacts the user’s experience.

Of course, this is not a big concern for static and textual content. But in the case of displaying animations, this number is critical.

If the React reconciliation algorithm traverses the entire App tree each time there is an update, rerenders it, and the traversal takes more than 16ms, it will drop frames.

This is a big reason why many wanted updates categorized by priority and not blindly applying every update passed down to the reconciler. Also, many wanted the ability to pause and resume work in the next frame. This way, React could have better control over working with the 16ms rendering budget.

This led the React team to rewrite the reconciliation algorithm, which is called Fiber. So, let’s look at how Fiber works to solve this problem.

How does React Fiber work?

Now that we know what motivated Fiber’s development, let’s summarize the features needed to achieve it. Again, I am referring to Andrew Clark’s notes for this:

  • Assign priority to different types of work
  • Pause work and come back to it later
  • Abort work if it’s no longer needed
  • Reuse previously completed work

One of the challenges with implementing something like this is how the JavaScript engine works and the lack of threads in the language. To understand this, let’s briefly explore how the JavaScript engine handles execution contexts.

The JavaScript execution stack

Whenever you write a function in JavaScript, the JavaScript engine creates a function execution context.

Each time the JavaScript engine starts, it creates a global execution context that holds the global objects; for example, the window object in the browser and the global object in Node.js. JavaScript handles both contexts using a stack data structure also known as the execution stack.

So, when you write something like this, the JavaScript engine first creates a global execution context and pushes it into the execution stack:

function a() {
  console.log("i am a")
  b()
}

function b() {
  console.log("i am b")
}

a()

Then, it creates a function execution context for the a() function. Since b() is called inside a(), it creates another function execution context for b() and pushes it into the stack.

When the b() function returns, the engine destroys the context of b(). When we exit the a()function, the a() context is destroyed. The stack during execution looks like this:

Execution Stack Diagram

But, what happens when the browser makes an asynchronous event like an HTTP request? Does the JavaScript engine stock the execution stack and handle the asynchronous event, or does it wait until the event completes?

The JavaScript engine does something different here: on top of the execution stack, the JavaScript engine has a queue data structure, also known as the event queue. The event queue handles asynchronous calls like HTTP or network events coming into the browser.

Event Queue Diagram

The JavaScript engine handles the items in the queue by waiting for the execution stack to empty. So, each time the execution stack empties, the JavaScript engine checks the event queue, pops items off the queue, and handles the event.

It is important to note that the JavaScript engine checks the event queue only when the execution stack is empty or the only item in the execution stack is the global execution context.

Although we call them asynchronous events, there is a subtle distinction here: the events are asynchronous with respect to when they arrive into the queue, but they’re not really asynchronous with respect to when they are actually handled.

Coming back to our stack reconciler, when React traverses the tree, it does so in the execution stack. So, when the updates arrive, they arrive in the event queue (sort of). And, only when the execution stack empties, the updates are handled.

This is precisely the problem Fiber solves by almost reimplementing the stack with intelligent capabilities—pausing, resuming, and aborting, for example.

Again referencing Andrew Clark, “Fiber is reimplementation of the stack, specialized for React components. You can think of a single fiber as a virtual stack frame.

“The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them however and whenever you want. This is crucial for accomplishing the goals we have for scheduling.

“Aside from scheduling, manually dealing with stack frames unlocks the potential for features such as concurrency and error boundaries. We will cover these topics in future sections.”

In simple terms, a fiber represents a unit of work with its own virtual stack. In the previous implementation of the reconciliation algorithm, React created a tree of objects (React elements) that are immutable and traversed the tree recursively.

In the current implementation, React creates a tree of fiber nodes that can mutate. The fiber node effectively holds the component’s state, props, and underlying DOM element it renders to.

And, since fiber nodes can mutate, React doesn’t need to recreate every node for updates; it can simply clone and update the node when there is an update.

In the case of a fiber tree, React doesn’t perform recursive traversal. Instead, it creates a singly-linked list and performs a parent-first, depth-first traversal.

Singly-linked list of fiber nodes

A fiber node represents a stack frame and an instance of a React component. A fiber node comprises the following members:

  • Type
  • Key
  • Child
  • Sibling
  • Return
  • Alternate
  • Output

Type

<div> and <span>, for example, host components (strings), classes, or functions for composite components.

Key

The key is the same as the key we pass to the React element.

Child

Represents the element returned when we call render() on the component:

const Name = (props) => {
  return(
    <div className="name">
      {props.name}
    </div>
  )
}

The child of <Name> is <div> because it returns a <div> element.

Sibling

Represents a case where render returns a list of elements:

const Name = (props) => {
  return([<Customdiv1 />, <Customdiv2 />])
}

In the above case, <Customdiv1> and <Customdiv2> are the children of <Name>, which is the parent. The two children form a singly-linked list.

Return

Return is the return back to the stack frame, which is a logical return back to the parent fiber node, and thus, represents the parent.

pendingProps and memoizedProps

Memoization means storing the values of a function execution’s result so you can use it later, thereby avoiding recomputation. pendingProps represents the props passed to the component, and memoizedProps initializes at the end of the execution stack, storing the props of this node.

When the incoming pendingProps are equal to memoizedProps, it signals that the fiber’s previous output can be reused, preventing unnecessary work.

pendingWorkPriority

pendingWorkPriority is a number indicating the priority of the work represented by the fiber. The ReactPriorityLevel module lists the different priority levels and what they represent. With the exception of NoWork, which is zero, a larger number indicates a lower priority.

For example, you could use the following function to check if a fiber’s priority is at least as high as the given level. The scheduler uses the priority field to search for the next unit of work to perform:

function matchesPriority(fiber, priority) {
  return fiber.pendingWorkPriority !== 0 &&
         fiber.pendingWorkPriority <= priority
}

Alternate

At any time, a component instance has at most two fibers that correspond to it: the current fiber and the in-progress fiber. The alternate of the current fiber is the fiber in progress, and the alternate of the fiber in progress is the current fiber.

The current fiber represents what is rendered already, and the in-progress fiber is conceptually the stack frame that has not returned.

Output

The output is the leaf nodes of a React application. They are specific to the rendering environment (for example, in a browser app, they are div and span). In JSX, they are denoted using lowercase tag names.

Conceptually, the output of a fiber is the return value of a function. Every fiber eventually has an output, but the output is created only at the leaf nodes by host components. The output is then transferred up the tree.

The output is eventually given to the renderer so that it can flush the changes to the rendering environment. For example, let’s look at how the fiber tree looks for an app with the following code:

const Parent1 = (props) => {
  return([<Child11 />, <Child12 />])
}

const Parent2 = (props) => {
  return(<Child21 />)
}

class App extends Component {
  constructor(props) {
    super(props)
  }
  render() {
    <div>
      <Parent1 />
      <Parent2 />
    </div>
  }
}

ReactDOM.render(<App />, document.getElementById('root'))

We can see that the fiber tree is composed of singly-linked lists of child nodes linked to each other (sibling relationship) and a linked list of parent-to-child relationships. This tree can be traversed using a depth-first search.

Fiber Tree Diagram

Render phase

To understand how React builds this tree and performs the reconciliation algorithm on it, let’s look at a unit test in the React source code with an attached debugger to follow the process; you can clone the React source code and navigate to this directory.

To begin, add a Jest test and attach a debugger. This is a simple test for rendering a button with text. When you click the button, the app destroys the button and renders a <div> with different text, so the text is a state variable here:

'use strict';

let React;
let ReactDOM;

describe('ReactUnderstanding', () => {
  beforeEach(() => {
    React = require('react');
    ReactDOM = require('react-dom');
  });

  it('works', () => {
    let instance;

    class App extends React.Component {
      constructor(props) {
        super(props)
        this.state = {
          text: "hello"
        }
      }

      handleClick = () => {
        this.props.logger('before-setState', this.state.text);
        this.setState({ text: "hi" })
        this.props.logger('after-setState', this.state.text);
      }

      render() {
        instance = this;
        this.props.logger('render', this.state.text);
        if(this.state.text === "hello") {
        return (
          <div>
            <div>
              <button onClick={this.handleClick.bind(this)}>
                {this.state.text}
              </button>
            </div>
          </div>
        )} else {
          return (
            <div>
              hello
            </div>
          )
        }
      }
    }
    const container = document.createElement('div');
    const logger = jest.fn();
    ReactDOM.render(<App logger={logger}/>, container);
    console.log("clicking");
    instance.handleClick();
    console.log("clicked");

    expect(container.innerHTML).toBe(
      '<div>hello</div>'
    )

    expect(logger.mock.calls).toEqual(
      [["render", "hello"],
      ["before-setState", "hello"],
      ["render", "hi"],
      ["after-setState", "hi"]]
    );
  })

});

In the initial render, React creates a current tree that renders initially.

createFiberFromTypesAndProps() is the function that creates each React fiber using the data from the specific React element. When we run the test, put a breakpoint at this function, and look at the call stack:

createFiberFromTypeAndProps() Call Stack

As we can see, the call stack tracks back to a render() call, which eventually goes down to createFiberFromTypeAndProps(). There are a few other functions that are of interest here: workLoopSync(), performUnitOfWork(), and beginWork().

workLoopSync()

workLoopSync() is when React starts building up the tree, starting with the <App> node and recursively moving on to <div>, <div>, and <button>, which are the children of <App>. The workInProgress holds a reference to the next fiber node that has work to do.

performUnitOfWork()

performUnitOfWork() takes a fiber node as an input argument, gets the alternate of the node, and calls beginWork(). This is the equivalent of starting the execution of the function execution contexts in the execution stack.

beginWork()

When React builds the tree, beginWork() simply leads up to createFiberFromTypeAndProps() and creates the fiber nodes. React recursively performs work and eventually performUnitOfWork() returns a null, indicating that it has reached the end of the tree.

Using instance.handleClick()

Now, what happens when we perform instance.handleClick(), which clicks the button and triggers a state update? In this case, React traverses the fiber tree, clones each node, and checks whether it needs to perform any work on each node.

When we look at the call stack of this scenario, it looks something like this:

instance.handleClick() Call Stack

Although we did not see completeUnitOfWork() and completeWork() in the first call stack, we can see them here. Just like performUnitOfWork() and beginWork(), these two functions perform the completion part of the current execution, which means returning back to the stack.

As we can see, together these four functions execute the unit of work and give control over the work being done currently, which is exactly what was missing in the stack reconciler.

The image below shows that each fiber node is composed of four phases required to complete that unit of work.

Fiber Node Diagram

It’s important to note here that each node doesn’t move to completeUnitOfWork() until its children and siblings return completeWork().

For instance, it starts with performUnitOfWork() and beginWork() for <App/>, then moves on to performUnitOfWork() and beginWork() for Parent1, and so on. It comes back and completes the work on <App> once all the children of <App/> complete work.

This is when React completes its render phase. The tree that’s newly built based on the click() update is called the workInProgress tree. This is basically the draft tree waiting to be rendered.

Commit phase

Once the render phase completes, React moves on to the commit phase, where it basically swaps the root pointers of the current tree and workInProgress tree, thereby effectively swapping the current tree with the draft tree it built up based on the click() update.

Commit Phase Diagram

Not just that, React also reuses the old current after swapping the pointer from root to the workInProgress tree. The net effect of this optimized process is a smooth transition from the previous state of the app to the next state and the next state, and so on.

And what about the 16ms frame time? React effectively runs an internal timer for each unit of work being performed and constantly monitors this time limit while performing the work.

The moment the time runs out, React pauses the current unit of work, hands the control back to the main thread, and lets the browser render whatever is finished at that point.

Then, in the next frame, React picks up where it left off and continues building the tree. Then, when it has enough time, it commits the workInProgress tree and completes the render.

Conclusion

I hope you enjoyed reading this post. Please feel free to leave comments or questions if you have any.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Karthik Kalyanaraman Software engineer. Curious about technology and the economics of the tech industry. Follow me on Twitter @karthikkalyan90.

4 Replies to “A deep dive into React Fiber”

  1. First of all thanks for the very well informed documentation for understanding few very fundamental implementation of JS. Just one question I wanted to put, where you were describing asynchronous behaviour of JS execution I feel you missed one part that before adding to queue tasks assigned to web api’s in case of browser. Please can you also explain that. Also please help me correcting if my understanding is wrong.

Leave a Reply