React performance hooks and compiler
React recently introduced a new compiler that can automatically optimize our
components. This means no more sprinkling useMemo
and useCallback
everywhere
š (but not in all cases). Letās see what problems it solves in practice.
Manual optimization
Firstly, hereās an example that I like to go through on an interview to gauge how knowledgeable the candidate is with React performance hooks:
function TodoList({ visibility, themeColor }) {
const [todos, setTodos] = useState(initialTodos);
const handleChange = (todo) => setTodos((todos) => getUpdated(todos, todo));
const filtered = todos.filter((todo) => todo.category === visibility);
return (
<div>
<ul>
{filtered.map((todo, index) => (
<Todo key={index} todo={todo} onChange={handleChange} />
))}
</ul>
<AddTodo setTodos={setTodos} themeColor={themeColor} />
</div>
);
}
This component is pretty straightforward, and there are a couple of optimizations that we can add.
useMemo
And the first one is useMemo
. This hook is intended for caching data to
avoid re-computing it on every render.
We can wrap the filter with it:
const filtered = useMemo(
() => todos.filter((todo) => todo.category === visibility),
[todos, visibility],
);
After adding it, when an unrelated piece of state changes, for example, themeColor
,
this piece of state wonāt be recomputed, but returned from cache instead. It will
only change when one of the dependencies changes. But thatās only the first part.
To ensure that <Todo />
components arenāt re-rendered unnecessarily, we also
need to take care of handleChange
function, because itās recreated on every render!
useCallback
Thatās exactly where this hook comes in: it allows us to cache functions.
const handleChange = useCallback(
(todo) => setTodos((todos) => getUpdated(todos, todo)),
[],
);
The syntax of it is essentially the same: the first argument is the function that we want to cache, and the second is an array of dependencies. In this case, itās empty because weāve used the special form of set state with a callback.
React.memo
But, turns out, itās not enough. Even though all of the props passed to the
<Todo />
component arenāt changing, React is still re-rendering them.
To mitigate this, the component itself needs to be memoized:
const TodoMemoized = React.memo(Todo);
React.memo
is a higher-order component that wraps a component, and by comparing
previous and new props decides whether to re-render the component or not.
Hereās what the manual optimization approach looks like after applying all of the optimizations:
const TodoMemoized = React.memo(Todo);
function TodoList({ visibility, themeColor }) {
const [todos, setTodos] = useState(initialTodos);
const handleChange = useCallback(
(todo) => setTodos((todos) => getUpdated(todos, todo)),
[],
);
const filtered = useMemo(
() => todos.filter((todo) => todo.category === visibility),
[todos, visibility],
);
return (
<div>
<ul>
{filtered.map((todo, index) => (
<TodoMemoized key={index} todo={todo} onChange={handleChange} />
))}
</ul>
<AddTodo setTodos={setTodos} themeColor={themeColor} />
</div>
);
}
Compiler
Letās rewind to the first example. Turns out all of the optimizations weāve just discussed now can be applied automatically!
Hereās a playground with this example:
I appreciate how effortless the setup was: I just had to install the Babel plugin and enable it in the configuration. Iāve also added updated ESLint rules for React hooks (which now include additional recommendations).
This is a remarkable achievement; now the code looks a lot more like the developer intended - without all of the intricate details of memoization.
But also, manual optimizations are still needed in some cases. Iāve encountered
it when Iāve enabled the compiler for visualizations on my website. Even though
Iāve removed a couple of useMemo
usages, I still had to leave the useCallback
s
that were needed for maze generation. Thatās because some functions were called
inside of the useEffect.
The React compiler doesnāt make performance hooks obsolete ā but it lets us focus more on logic, less on micromanaging renders.
Conclusion
Iām excited to install and try out this compiler on my project at work! Given how easy the setup was for my blog, Iām hoping that it will be as easy on the ārealā project. The project at work is far more complex and much bigger, but Iāll share how it goes.
But only after I merge this monstrous PR to upgrade from React 17 to 18 š