Before You Use `memo`
Translated from https://overreacted.io/before-you-memo/
There are many articles out there about how to optimize the performance of a React application. Typically, if state updates are slow, you need to:
Ensure you are building a production build (development builds are deliberately slow, in extreme cases even an order of magnitude slower)
Make sure you haven't placed state at an unnecessarily high level (for example, putting the
inputstate in some centralized state management is usually not a good idea)Run
React DevTools Profilerto see what causes re-renders, and wrap the most expensive re-renders withmemo/useMemo.
The last step is usually the most annoying. Ideally, a compiler should do this for you, and it might be possible in the future.
In this blog post, I want to share two different technical approaches. Although these techniques are very basic, few people realize they can improve performance.
A (Artificially) Slow Component
Here is a component with a severe rendering performance problem:
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) {
// Artificial delay -- do nothing for 100ms
}
return <p>I am a very slow component tree.</p>;
}
The problem is that whenever color changes, ExpensiveTree re-renders, and this re-render has been artificially made very slow.
I could use memo and be done with it, but there are already many articles about memo, so I won't spend time on that. I want to show two different solutions.
Solution 1: Lift State Down
If you look closely at the rendering code, you'll notice that only part of the returned tree actually cares about the current color state. So we can extract that part into a Form component and move the color state down into the Form component:
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>
</>
);
}
Now when color changes, only Form re-renders, problem solved.
Solution 2: Lift Content Up
But what if the state change occurs in a parent of the component that has re-rendering performance issues? The above solution won't work then. For example:
export default function App() {
let [color, setColor] = useState("red");
return (
// color is used in a parent of ExpensiveTree
<div style={{ color }}>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<p>Hello, world!</p>
<ExpensiveTree />
</div>
);
}
Now we can't solve the problem by extracting the part that doesn't use color into another component, because that would have to include the div, and the div contains ExpensiveTree. So we just have to use memo, right?
...
...
...
...
Actually, it's simple:
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>
);
}
We split the App component into two. The part that depends on color is moved into ColorPicker. The part that doesn't care about color stays in the App component and is passed to ColorPicker via children.
When color changes, ColorPicker re-renders, but the children prop hasn't changed, so the subtree is not visited by React.
The conclusion is that <ExpensiveTree /> does not re-render.
What is the Best Practice
Before applying optimizations like memo or useMemo, it often makes sense to do the kind of component splitting described above.
What’s interesting about these approaches is that they have nothing to do with performance per se. Splitting components with children makes the data flow clearer and reduces the number of props passed across components. In this case, the performance improvement is just a bonus, not the end goal.
Oddly enough, this pattern also brings more performance benefits in the future.
For example, when the Server Components feature stabilizes and is officially released, our ColorPicker component could receive its children from the server. The entire <ExpensiveTree/> component, or parts of it, could run on the server, and even updates to the top-level state on the client could "skip" re-rendering those parts.
That's something even memo can't do! But again, these two approaches are complementary. Use Profiler and memo when both Lifting Content Up and Lifting State Down are not enough.