Hello, everyone. I am Karsong.

For this common interaction step:

  1. Click the button to trigger a status update

  2. Component render

  3. The view to render

Which steps do you think have room for performance optimization?

The answers are: 1 and 2.

For Step 1, if there is no change before and after the status update, skip the following steps. This optimization strategy is called eagerState.

For step 2, you can skip the render of the descendant component if there is no state change for the descendant node of the component. This optimization strategy was called Bailout.

It seems that the logic of eagerState is simple, just comparing whether there has been a change before and after the status update.

In practice, however, it is complicated.

By looking at the logic of eagerState, this article answers the question: Is React performance optimized to the Max?

Welcome to join the Human high quality front-end framework research group, Band fly

A strange example

Consider the following components:

function App() {
  const [num, updateNum] = useState(0);
  console.log("App render", num);

  return (
    <div onClick={()= > updateNum(1)}>
      <Child />
    </div>
  );
}

function Child() {
  console.log("child render");
  return <span>child</span>;
}
Copy the code

Online Demo Address

First render, print:

App render 0
child render 
Copy the code

Click div for the first time and print:

App render 1
child render 
Copy the code

Click div a second time and print:

App render 1
Copy the code

Third, fourth…… Click div twice, do not print

On the second click, App Render 1 is printed, not Child Render. App descendant component without Render, hit Bailout.

The third and subsequent click, which prints nothing, means no component render, hits eagerState.

EagerState failed to hit eagerState on the second click. EagerState failed to hit eagerState on the second click. EagerState failed to hit eagerState on the second click.

Trigger condition of eagerState

The first thing we need to understand is, why eagerState?

Usually, when do you get the latest status? When the component renders.

When the component render, useState executes and returns the latest state.

Consider the following code:

const [num, updateNum] = useState(0);
Copy the code

The num returned by useState is the latest status.

The most recent state is calculated at useState execution because the state is calculated based on one or more updates.

For example, three updates are triggered in the following click event:

const onClick = () = > {
  updateNum(100);
  updateNum(num= > num + 1);
  updateNum(num= > num * 2);
}
Copy the code

What is the latest state of num when the component renders?

  • First, num becomes 100

  • 100 plus 1 is 101

  • 101 times 2 is 202

Therefore, useState returns 202 as the latest state of num.

The actual situation is more complicated, updates have their own priority, so it is not clear until render which updates will participate in the state calculation.

So, in this case the component must render and useState must execute to know what the latest state of num is.

The latest state of num cannot be compared with the current state of num to determine whether the state has changed.

The significance of eagerState is that, in some cases, we can calculate the latest state before the component renders (this is where eagerState comes from).

In this case, the component does not need render to compare state changes.

So what happens?

The answer is: when there is no update on the current component.

When no update exists, this update is the first update to the component. The latest status can be determined with only one update.

So, the premise of eagerState is:

There are no updates for the current component, so the first time a state update is triggered, the latest state can be calculated immediately and compared to the current state.

If the two are consistent, the process of subsequent render is omitted.

That’s the logic of eagerState. Unfortunately, it’s a little more complicated than that.

Let’s take a seemingly unrelated example first.

Necessary React source code knowledge

For the following components:

function App() {
  const [num, updateNum] = useState(0);
  window.updateNum = updateNum;

  return <div>{num}</div>;
}
Copy the code

Can I change the num displayed in the view by executing the following code on the console?

window.updateNum(100)
Copy the code

The answer is: yes.

Because fiber (the node that holds component information) for the App component is already passed as a default parameter to window.updatenum:

// The updateNum implementation looks something like this
// Fiber is the corresponding fiber of App
const updateNum = dispatchSetState.bind(null, fiber, queue);
Copy the code

Therefore, the fiber corresponding to App can be obtained when updateNum is executed.

However, a component actually has 2 fibers, and they:

  • One that stores information corresponding to the current view is called current Fiber

  • One that holds information about the view to be changed next is called WIP Fiber

Wip Fiber is the default in updateNum.

When a component triggers an update, the update will be marked on both fibers corresponding to the component.

When the component renders, useState executes, calculates the new state, and clears the update mark on wIP Fiber.

When the view is rendered, current Fiber and WIP Fiber will switch locations (i.e., the current WIP Fiber will become the next current Fiber).

Back to the example

As mentioned earlier, the premise of eagerState is that there is no update to the current component.

Specifically, there is no update in current Fiber and WIP Fiber corresponding to the component.

Back to our example:

Click div for the first time and print:

App render 1
child render 
Copy the code

Current Fiber and WIP Fiber are marked and updated at the same time.

Wip Fiber update mark cleared after render.

At this time, current Fiber still has the update mark.

After rendering, Current fiber and WIP Fiber will switch locations.

Instead: WIP Fiber has been updated, while current fiber has not been updated.

So when I click div a second time, I miss eagerState due to an update in WIP Fiber, so I print:

App render 1
Copy the code

Wip Fiber update mark cleared after render.

There is no update mark on either fiber. So any subsequent div click will trigger eagerState and the component will not render.

conclusion

React performance optimizations can sometimes confuse developers because of the interplay between the various parts of React.

Why don’t we hear a lot of complaints? Because the performance optimization is only reflected in the metrics, it does not affect the interaction logic.

In this paper, we found that the React performance optimization was not perfect, and the eagerState strategy did not reach the optimal state due to the existence of two fibers.