• A deep dive into React Fiber internals
  • Originally written by Karthik Kalyanaraman
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: MarchYuanx
  • Proofread by JohnieXu CoolRice

Learn more about React Fiber’s internal implementation

Have you ever wondered what happens inside React when reactdom.render (
, document.getelementByid (‘root’)) is called?

We know that ReactDOM builds the DOM tree in the background and renders the application on the screen. So how does React actually build the DOM tree? How does an application update the DOM tree when its state changes?

In this article, I’ll explain how React builds DOM trees before React 15.0.0, and how it doesn’t work, and then explain the new DOM rendering mechanism in React 16.0.0. This article will cover a great deal of detail about the internal implementation of React, which may not be necessary for regular project development using React.

Stack the coordinator

Let’s start with the previously mentioned reactdom.render (
, document.getelementByid (‘root’)) code.

Here the ReactDOM receives the
as a parameter and passes it to the Reconciler. You may have two questions:

  1. <App />What does it mean?
  2. And what is the Reconciler?

I’ll answer both of these questions.

is a React element that describes elements of the DOM tree.

“The React element is a generic object that describes a component instance or DOM node and its desired properties.” – the React blog

In other words, the React element is not a real DOM node or component instance, but rather a way of describing the type of DOM element, the properties it owns, and the child elements it contains.

This is at the heart of React, which abstracts the complex logic of building, rendering, and managing the life cycle of a real DOM tree, effectively simplifying developers’ lives. To fully understand the uniqueness of this approach, we can compare how it is handled using traditional object-oriented thinking.

In the typical object-oriented programming world, developers need to instantiate and manage the life cycle of each DOM element. For example, if a developer wants to create a simple form and a submit button, even their simple state management needs to be maintained by the developer alone.

Assume that the Button component has a state variable isSubmitted. The life cycle of a Button component is similar to the following flowchart, where each state needs to be handled by the application:

The size and lines of code of the flowchart grow exponentially as the number of states increases.

React uses elements to solve this problem subtly. React has two elements:

  • DOM element: when the element is of type string, for example<button class="okButton"> OK </button>
  • Component element: when the type is a class or function, for example<Button className="okButton"> OK </Button>, including<Button>Is one of the typical class components, function components we use

It is important to understand that both types are simple objects. They are just descriptions of what needs to be rendered on the screen, and no actual rendering takes place when you create and instantiate them. This makes React easier to parse and iterate over them to build DOM trees. The actual rendering takes place after the traversal is complete.

When React encounters a class or a function component, it asks the element how the element should be rendered according to its props. For example, if the

component renders the following:

<Form>
  <Button>
    Submit
  </Button>
</Form>
Copy the code

React then asks the

and

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

React calls Render () to see what elements it renders, and you’ll eventually see that it renders a

with child elements. React will repeat this process until it knows the underlying DOM tag element for each component on the page.

The exact process of recursively traversing the tree to understand the underlying DOM tag elements of the React application component tree is called coordination. At the end of the coordination, React knows the result of the DOM tree, and renderers like React-DOM or React-Native will apply the minimal set of changes required to update the DOM nodes.

Therefore, this means that React will perform coordination when you call reactdom.render () or setState(). In the case of setState, it performs a walk and finds out what has changed in the tree by distinguishing the new tree from the rendered one. These changes are then applied to the current tree to update the state associated with the setState() call.

Now that we know what coordination is, let’s look at the pitfalls of this pattern.

Oh, by the way — why is this called the “stack” coordinator?

The name is derived from the “stack” data structure, which is a last-in, first-out mechanism. How does the stack relate to what we just saw? Well, it turns out that since we’re actually recursing, it depends on the stack.

recursive

To understand why this happens, let’s take a simple example of what happens in the call stack.

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

fib(10)
Copy the code

As we can see, the call stack pushes each call to fib() until fib(1) goes off the stack, which is the first function call returned. It then continues the recursive call to the stack and exits the stack again when the return statement is reached. In this way, it actually uses the call stack until fib(3) returns and becomes the last item out of the stack.

The coordination algorithm we just saw is a pure recursive algorithm. The update causes the entire child to be erected i.e. re-rendered. While this works well, there are some limitations. As Andrew Clark points out:

  • In the user interface, you don’t need to apply every update immediately; In practice, doing so can be wasteful, resulting in lost frames and a reduced user experience.
  • Different types of updates have different priorities — animated updates need to be completed faster than updates in the data store.

Now, when we say lost frames, what are we talking about? Why do recursive methods have this problem? To grasp this, let me briefly explain what frame rate is and why it matters from a user experience perspective.

Frame rate is the frequency at which a continuous image appears on the display. Everything we see on a computer screen is made up of frames or images that are played on the screen and displayed at a rate that is instantaneous.

To understand what this means, think of the computer screen as a page-turning book, and think of the pages of a page-turning book as the frames that play at a certain rate as the page is turned. In other words, a computer monitor is nothing more than a self-turning book that keeps playing as things change on the screen. If that’s not clear, watch this video.

In general, video needs to be played at 30 frames per second (FPS) for it to feel smooth and real-time to the human eye. Anything higher than this will provide a better experience. This is one of the main reasons why gamers prefer higher frame rates when playing first-person shooters; accuracy is very important.

Having said that, most devices refresh their screens at 60 FPS these days, in other words 1/60 = 16.67ms, which means a new frame is displayed every 16ms. This number is important because if the React renderer takes longer than 16ms to render something on the screen, the browser will lose frames.

But, in reality, the browser has “chores” to do, so all your work needs to be done in 10 ms. When you can’t meet that budget, the frame rate drops and the content on the screen shakes. This is often referred to as Jank and can negatively impact the user experience.

Of course, for static and textual content, this is not a big deal. But in the case of animation, this number is critical. Therefore, if the React coordinated algorithm traverses the entire App tree and re-renders every time there is an update, it will cause annoying frame loss issues if the traversal takes longer than 16 ms.

This is an important reason why it is better to sort the updates by priority rather than blindly apply every update passed to the coordinator. Another nice feature is the ability to pause and resume work in the next frame. As a result, React has more control over its rendering budget of 16 ms.

This led the React team to rewrite the coordination algorithm, called Fiber. I think it’s important to understand how Fiber exists, why it exists, and what it means. Let’s see how Fiber solves this problem.

Working principle of Fiber

Now that we know what motivated the development of Fiber, let’s summarize the features needed to implement Fiber.

Again, I’m going to quote Andrew Clark:

  • Prioritize different types of work
  • Suspend and resume work
  • If you no longer need it, stop working
  • Reuse previously completed work

One of the challenges of doing something like this is the way the JavaScript engine works, and to some extent the language lacks threads. To understand this, let’s briefly explore how the JavaScript engine handles the execution context.

JavaScript execution stack

Every time you write a function in JavaScript, the JS engine creates what is called the function execution context. In addition, every time the JS engine starts, it creates a global execution context that contains global objects — for example, the Window object in the browser and the global object in Node.js. Both of these contexts are handled in JS using stack data structures (also known as execution stacks).

So when you write something like this:

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

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

a()
Copy the code

The JavaScript engine first creates a global execution context and pushes it onto the execution stack. Then create the function execution context for the a() function. Since b() is called inside a(), it creates another function execution context for b() and pushes it onto the stack.

When function B () returns, the engine clears the context of b(), and when we exit function a(), it clears the context of a(). The stack during execution looks like this:

But what happens when the browser issues asynchronous events like HTTP requests? Does the JS engine store the execution stack and process the asynchronous event, or wait until the event completes?

The JS engine does something different here. At the top of the execution stack, the JS engine has queue data structures, also known as event queues. Event queues handle asynchronous calls to the browser, such as HTTP requests or network events.

The JS engine processes the contents of the queue by waiting for the execution stack to empty. Therefore, every time the execution stack becomes empty, the JS engine checks the event queue, ejects the items in it, and processes the event. Note that the JS engine only checks the event queue if the execution stack is empty or if there is only a global execution context in the execution stack.

Although we call them asynchronous events, there is a subtle difference: events are asynchronous relative to when they are queued, but they are not truly asynchronous relative to when they are actually processed.

Going back to our stack coordinator, React is executing in the execution stack as it traverses the tree. Therefore, when updates are obtained, they reach the event queue (sort of). Updates are processed only if the execution stack is empty. This is exactly the problem Fiber solves with smart functionality that almost reimplements the stack — pause, continue, abort, etc.

Again, to quote Andrew Clark:

“Fiber is a reimplementation of the stack, dedicated to the React component. You can think of individual fibers as frames of a virtual stack.

The advantage of reimplementing the stack is that you can keep stack frames in memory and execute them as needed (and at any time). This is critical to achieving the goals of our plan.

In addition to scheduling, manual handling of stack frames can open up features such as concurrency and error boundaries. We will cover these topics in future chapters.”

Simply put, a fiber is a unit of work with its own virtual stack. In previous implementations of the coordination algorithm, React created an immutable object tree (React elements) and recursively traversed the tree.

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

And because fiber nodes can change, React doesn’t need to recreate every node for updates — it can simply clone and update nodes when it updates. Also, React doesn’t do recursive traversal for fiber trees. Instead, you create a single linked list for parent-first, depth-first traversal.

Single-linked list of fiber nodes

A Fiber node represents a stack frame, as well as an instance of the React component. A Fiber node contains the following members:

type

,
, etc. of native components (strings), classes or functions of composite components.

health

Same key passed to the React element.

Child elements

Represents the element returned when we call Render () on the component. Such as:

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

The child element of

is

because it returns a

element.

Sibling elements

Represents the case where Render returns a list of elements.

const Name = (props) = > {
  return([<Customdiv1 />.<Customdiv2 />])}Copy the code

In this case,

and

are children of the parent element

. These two child elements form a singly linked list.


return

Represents a return stack frame, which logically returns to the parent Fiber node. Therefore, it represents the parent.

pendingPropsmemoizedProps

Memorization means storing the value of the result of a function’s execution so that it can be used later, thereby avoiding recalculation. PendingProps represents the props passed to the component, while memoizedProps is initialized at the end of the execution stack to store the props of that node.

When the pendingProps passed is equal to memoizedProps, it means that the output before Fiber can be reused, thus avoiding unnecessary work.

pendingWorkPriority

A number that represents the work priority of fiber. The ReactPriorityLevel module lists the different priorities and what they represent. With the exception of NoWork, which is zero, the higher the number, the lower the priority.

For example, you can use the following function to check whether a fiber has at least the same priority as a given level. The scheduler uses the priority field to search for the next unit of work to execute.

function matchesPriority(fiber, priority) {
  returnfiber.pendingWorkPriority ! = =0 &&
         fiber.pendingWorkPriority <= priority
}
Copy the code

The standby

At any time, a component instance can have at most two corresponding fibers: current fiber and ongoing fiber. They’re backups for each other. Currently fiber represents rendered content, while ongoing Fiber is conceptually a stack frame that has not yet been returned.

The output

The React application leaf node. They are specific to the rendering environment (for example, in browser applications, they are div, SPAN, and so on). In JSX, they are represented by lowercase tag names.

Conceptually, the output of fiber is the return value of the function. Each fiber eventually has an output, but the output is only created by native components on leaf nodes. The output will be sent to the tree.

The output is eventually fed to the renderer so that changes can be flushed to the rendering environment. For example, let’s see how the Fiber tree would find an application with code like this:

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'))
Copy the code

As you can see, the Fiber tree consists of a single linked list of interconnected child nodes (siblings) and a linked list of parent-child relationships. You can traverse the tree using a depth-first search.

Rendering phase

To understand how React builds this tree and performs a coordination algorithm on it, I decided to write a unit test in the React source code, with a debugger attached to track the process.

If you’re interested in this process, copy the React source code and navigate to this directory. Add a Jest test and attach a debugger. The test I wrote is a simple test that basically renders a button with text. When you click the button, the application destroys the button and renders a

with a different text, so the text is a state variable here.
'use strict';

let React;
let ReactDOM;

describe('ReactUnderstanding', () => {
  beforeEach((a)= > {
    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 = (a)= > {
        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"]] ); })});Copy the code

In the initial render, React creates a current tree, which is the tree that was originally rendered.

[createFiberFromTypeAndProps()](https://github.com/facebook/react/blob/f6b8d31a76cbbcbbeb2f1d59074dfe72e0c82806/packages / react – the reconciler/SRC/ReactFiber js# L593) is to use the data from the specific elements of the react to create each react fiber function. When we run the test, place a breakpoint at this function and look at the call stack, which looks like this:

As we have seen, the call stack will track to a render () call, the call will eventually return to createFiberFromTypeAndProps (). There are some other functions we are interested in: workLoopSync(), performUnitOfWork(), and beginWork().

function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while(workInProgress ! = =null) { workInProgress = performUnitOfWork(workInProgress); }}Copy the code

WorkLoopSync () is where React starts building the tree, starting at the

node and recursively going to

,

, and

PerformUnitOfWork () takes a fiber node as an input parameter, gets the standby node for that node, and calls beginWork(). This is equivalent to starting the function execution context in the execution stack.

When the React to build tree, beginWork () will only point to createFiberFromTypeAndProps () and create fiber node. React performs the work recursively, and eventually performUnitOfWork() returns null, indicating that it has reached the end of the tree.

Now, what happens when we execute instance.Handleclick (), basically clicking the button and triggering a status update? In this case, React walks through the Fiber tree, cloning each node, and checking if it needs to do some work on certain nodes. When we look at the call stack for this case, it looks like this:

Although we didn’t 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 of the current execution, which essentially means going back on the stack.

As we can see, these four functions together perform the work of the unit of work and also control what is currently being done, which is missing from the stack coordinator. As shown in the figure below, each Fiber node consists of four phases required to complete this unit of work.

Note here that each node does not move to completeUnitOfWork() until its children and siblings return completeWork(). For example, for
, it starts with performUnitOfWork() and beginWork(), for Parent1, goes to performUnitOfWork() and beginWork(), and so on. Once all the children of
have finished their work, it will go back and finish working on

.

This is when React finished its rendering phase. The new tree based on the Click () update is called the workInProgress tree. This is basically a draft tree waiting to be rendered.

The commit phase

After the render phase is complete, React enters the commit phase, where it basically swaps the root pointer to the current tree and the workInProgress tree, effectively swapping the current tree with the draft tree created based on the Click () update.

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

What about 16 ms frame time? React effectively runs an internal timer for each unit of work being executed and continuously monitors this time limit as work is executed. When the time is up, React pauses the unit of work currently in progress, gives it control to the main thread, and lets the browser render everything that’s done at that point.

Then, in the next frame, React picks up where it left off and continues building the tree. Then, when there is enough time, it submits the workInProgress tree and completes the rendering.

conclusion

Hope you enjoyed this article, and if you have any comments or questions, please leave them in the comments below.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.