Stefano J. Attardi

Why We Memo All the Things

October 28, 2020

On my team at Coinbase, we ask everyone to use the React performance trinity – memo, useMemo, and 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.

Why We React.memo All Components

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:

  • Use memo some of the time
  • Use memo all 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?

If we 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:

  1. Running a render function
  2. Allocating all callbacks anew
  3. Allocating all useMemo functions anew
  4. Allocating a bunch of new JSX elements
  5. Repeating 1–4 recursively for all the children
  6. Causing the React reconciler to compare the old tree with the new tree

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.

Sane Defaults

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?

CPU Cost of React.memo

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 memo introduces a measurable performance hit, ping me. I’ll be happy to update this post based on new information!

Memory Cost of React.memo

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.

Isn’t it Premature Optimization?

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.

Why we React.useCallback All Callbacks

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 memo work.

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.

Why we React.useMemo All the Props & Deps

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 useCallbacks and 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.

Will Someone Please Think of the Children?

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 memo.

How do we deal with this? The same way we deal with all other complex data structures created during render: we useMemo it.

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.

Conclusion

Using 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.

Stefano J. Attardi

Engineering Manager at Coinbase.

Get in Touch