There are many articles about React performance tuning

In general, if some state updates are slow, you need to:

  1. Verify that one is runningThe production environmentBuild. (The development environmentThe build will be deliberately slow,extremeMay be an order of magnitude slower.
  2. Verify whether the treeStates are placed in a higher position than they really need to be. (For example, the input boxstateI put it in the centerstoreIs probably not a good idea)
  3. runReact Developer ToolsTo determine what caused itThe second renderAnd, inWrap Memo () on expensive subtrees. (and use it where neededuseMemo())

The last step is annoying, especially for components in between

Ideally, the compiler can do this for you. Maybe in the future.

In this article, I want to share two different techniques. They are very basic, which is why people rarely realize that they can improve rendering performance.

These techniques complement what you already know. They won’t replace memo or useMemo, but it’s good to try them out first

A (manual) deceleration component

Here is a component with serious rendering performance issues

import { useState } from 'react';

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e)= > setColor(e.target.value)} />
      <p style={{ color}} >Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Manual delay -- 100 milliseconds to do nothing
  }
  return <p>I am a very slow component tree.</p>;
}
Copy the code

The problem is that when the color in the App changes, we will re-render the
component that we manually delayed greatly.

I could just write a memo() on it and call it a day, but there are plenty of articles out there, so I won’t spend time on how to optimize using memo().

I just want to show you two different solutions.

Solution 1: Move State down

If you take a closer look at the rendering code, you’ll notice that only part of the returned tree really cares about the current color:

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e)= > setColor(e.target.value)} />
      <p style={{ color}} >Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}
Copy the code

So let’s extract this into the Form component and move the state into it:

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>
  );
}

function Form() {
  let [color, setColor] = useState('red');
  return (
    <>
      <input value={color} onChange={(e)= > setColor(e.target.value)} />
      <p style={{ color}} >Hello, world!</p>
    </>
  );
}
Copy the code

Now if the color changes, only the Form will be rerendered. Problem solved.

Solution 2: Content enhancement

This solution doesn’t work when a portion of state is used in code above a high-overhead tree.

For example, if we put color inside the parent div element.

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div style={{ color}} >
      <input value={color} onChange={(e)= > setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}
Copy the code

Now it looks like we can’t extract the part that doesn’t use color into another component, because this part of the code will first include the parent component’s div, then include
.

You can’t avoid using memo at this point, can you? Or is there a way to avoid it?

export default function App() {
  return (
    <ColorPicker>
      <p>Hello, world!</p>
      <ExpensiveTree />
    </ColorPicker>
  );
}

function ColorPicker({ children }) {
  let [color, setColor] = useState("red");
  return (
    <div style={{ color}} >
      <input value={color} onChange={(e)= > setColor(e.target.value)} />
      {children}
    </div>
  );
}
Copy the code

We split the App component into two child components.

The code that depends on color goes into the ColorPicker component along with the color State variable.

The parts that don’t care about color remain in the App component and are passed to the ColorPicker as JSX content, also known as the children property.

When the color changes, the ColorPicker rerenders.

But it still holds the same children property it got from the App last time, so React doesn’t access those subtrees. Therefore ExpensiveTree does not re-render.

What’s the moral?

When you’re optimizing with Memo or useMemo, it might seem like it makes sense if you can separate the changing parts from the unchanged ones.

The interesting part about these approaches is that they are not really about performance per se

Using the children attribute to split components generally makes the application’s data flow easier to track and reduces the number of props across the tree

Improving performance in this case is the icing on the cake, not the end goal.

Oddly enough, this pattern will provide even more performance benefits in the future

For example, when the server component is stable and adoptable, our ColorPicker component can get its children from the server.

The entire
component or parts of it can run on the server, and even top-level React status updates “skip” these parts on the client

That’s something Memo can’t do! However, the two approaches are complementary. Don’t ignore the state drop (and content boost!)

Haven’t I read this before?

Probably is

This is not a new idea. This is just a natural consequence of a React combination model. It is too simple to be appreciated, yet it deserves more love.