On my team at Coinbase, we ask everyone to use the React performance trinity –
useCallback – all the time. For some reason, this is controversial. I’m guessing this has something to do with Twitter. This article explains why we do it anyway.
Let’s start with what we can all agree on: in most apps, some components can benefit from being wrapped in
React.memo. Maybe because they are expensive to rerender, or maybe they are children of a component that renders much more frequently. Maybe both.
So not using
memo at all is not an option. We are left with two options:
memosome of the time
memoall the time
The first option sounds like the most appealing, doesn’t it? Figure out when we can benefit from
React.memo, and use it then, and only then. However, before we go that far, we have to remind ourselves that we work on a large team. No matter how diligent we are with education, code review, and profiling, we are not going to get it right 100% of the time. So we have to ask ourselves:
What is the cost of getting it wrong?
memo a component that doesn’t need to be, all we’re doing is a shallow equality check on the props of that component, on each potential render.
If we don’t
memo a component that should be, we are:
If you’ve ever profiled a React app – even in production mode – you know there is a non-negligible performance impact to every component that renders. By contrast, the props comparison in
memo itself hardly ever shows up in profiles.
Wastefully rerendering a component is more expensive than wastefully testing whether props changed. So we want to err on the side of avoiding unnecessary rerenders. Since we are fallible, the only foolproof way to achieve that is
memoing everything by default.
On top of that, leaving the responsibility of deciding when to use
memo and when not to places an unnecessary burden on all our engineers. Do we expect everybody to be intimately familiar with the tradeoffs? To profile each component to make the decision? What is the cost in terms of extra mental effort and time to try to get this right? Is it worth it? Why not provide a sane default, and diverge when necessary?
But, you may be thinking, what if the vast majority of my components are cheap to rerender? Won’t the cost of all these unnecessary
memos add up to more than the possible cost of a wasted expensive rerender?
In my experience, the answer is no. I have never seen
memo itself show up in a profile, whereas it’s pretty common to see expensive renders take mega amounts of CPU time. If you see something different, you probably have a bigger problem, like having way too many components mounted.
If you have a legit use case where
memointroduces a measurable performance hit, ping me. I’ll be happy to update this post based on new information!
There is a
memo meme that’s going around: they say that
React.memo has a memory cost, because – like other memoizations – you have to keep around the old values in case you need them again later. Makes sense? Unfortunately, that’s not quite right. Because of how React works, the result of previous renders needs to be kept around anyway – to diff it against subsequent renders. That’s the basis of React’s reconciliation algorithm. It can’t work without it.
Don’t take my word for it. Here’s Christopher Chedeau on Twitter, responding to one Dan Abramov:
I don’t think that it’s a great analogy. Doing memoize() on every function would be horrible because you’d have to store the state of the input/output for all the calls. In the React case, React already does that for everything, so it’s “free”.
In case you don’t know him, Christopher Chedeau has been on the React Core team at Facebook since 2012, i.e. the very beginning. He created React Native, Prettier, CSS-in-JS, Excalidraw, and Yoga.
Premature optimization is spending time optimizing your code for performance before you know which code needs to be made faster. By asking engineers to make the memo-no-memo choice on every component, we are forcing them to spend more time thinking about performance because we are concerned about the potential performance cost of a
memo call. That, in my mind, is premature optimization. Not the other way round.
Are we on the same page about
React.memo? Good, then this one is going to be easy. In the majority of the cases, callbacks get passed as props to other components. If you don’t wrap them in
useCallback, you’re gonna break
memo. It’s that simple. Wrap your callbacks in
useCallback to make
What about callbacks that get passed to primitive components? Isn’t
useCallback useless in those cases? Yes. But. When the next intern wraps your primitive in another component, are they gonna know to go back and change all the passed in callbacks to wrap them in
useCallback? Probably not.
Plus, the arguments above about having sane defaults and not creating an extra burden on our engineers still apply. The CPU and memory cost of
useCallback is negligible. Yes, callbacks need to stick around in memory regardless. After all, they may need to be called!
Keep it simple.
useCallback all the fns.
The same thing goes for any time we are creating a new object or array. We have to wrap it in
useMemo or it’s going to break any component that receives those values as props.
Any data structure that gets recreated on every render can also break downstream
useMemos, by showing up in their list of dependencies. Such ever-changing values are then referenced in other callbacks and derived values, so the breakage spreads – like a coronavirus in a town where only half the people wear masks. If things aren’t
memoed by default, debugging performance issues as they come up is going to involve a long game of whack-a-mole, as you walk back up the chain of dependencies all the way to add memoization at every step.
Many people aren’t aware of this:
children is a sneaky prop that breaks
memo. JSX creates a new data structure on every render. Any time a component rerenders and passes JSX to another component, it’s gonna break the child component’s
How do we deal with this? The same way we deal with all other complex data structures created during render: we
I know what you’re thinking: nobody wraps children in
useMemo, and you’re right. I’ll admit that we also don’t often do this in our codebase. It’s just not idiomatic React, and doing this everywhere might be too much to ask. So we live with this compromise. This means we have to be vigilant, and continuously monitor our app for performance issues. The good news is, when we identify one, wrapping a
useMemo around a children prop is a lot more straightforward than adding all sorts of optimizations to a complex web of hooks and components that started off with zero optimizations to begin with.
memo and its cousins everywhere is a sane default. It’s like a mask mandate during a coronavirus pandemic. Sure, we could have everybody tested every day and only ask people who are actively contagious to wear a mask. But it’s far cheaper, simpler, and ultimately more effective to ask everybody to wear a mask by default.