Frontend Weekly
Published in

Frontend Weekly

How to settle the React memoization debate

Memoization is a deep topic that many developers are passionate about. As such, there are many opinions flying around about how memoization should be used. It is a very common debate amongst front end engineers at companies that use React. The goal of this article is to provide one such opinion about how memoization should be used and it was written as a result of several discussions with engineers where I work.

The code samples in this article are from the memoization-workshop project. I encourage you to clone the repo, run it locally and follow the instructions in the README.md to be hands on with the code and to learn the tools.

What is memoization?

Memoization is an optimization that functional programming languages and frameworks use to avoid re-computation of repeated function calls when inputs have not changed. Think of it like caching the results of function calls where the hash key is made up of the arguments to that function. In order to memoize a function, it must be a pure function without any side effects (for example reliance on global variables).

Let’s take a look at a basic implementation of memoization below.

const memoize = func => {
const cache = {};
return (...args) => {
// Create a key from the arguments
const argsKey = args.join(',');
if (!args.length) {
return func.apply(this);
}
if (!cache[argsKey]) {
console.log('Not found in cache. Adding it.');
cache[argsKey] = func.apply(this, args);
} else {
console.log('Found in cache.');
}
return cache[argsKey];
}
}
const shoutIt = (str) => `${str.toUpperCase()}!`;
const memoizedShoutIt = memoize(shoutIt);

If you run the above in a REPL (your browser’s console, for example) you will get:

// memoizedShoutIt('hello') // Not found in cache. // "HELLO!"
// memoizedShoutIt('hello') // Found in cache. // "HELLO!"

How does React use memoization?

When you strip away JSX and all the complexity of React, what you’re left with are functions that take arguments (props) and return an object (React element). Using React.createElement instead of JSX, you can see this.

For example, run the following code in a node REPL or in a browser window (you will need to have React in scope).

const ShoutIt = ({ text }) => {
const shoutedText = `${text.toUpperCase()}!`;
return React.createElement('p', {
children: [shoutedText]
})
}
console.log(JSON.stringify(ShoutIt('Hello World'))){
'$$typeof': Symbol(react.element),
type: 'p',
key: null,
ref: null,
props: { children: [ 'HI!' ] },
_owner: null,
_store: {}
}

Behind the scenes, React has a reconciliation abstraction that manages updating the DOM when inputs change by recomputing function calls and batching updates using React’s new Fiber reconcilliation (see for more details).

All that is to say, React is fundamentally just a way to update the DOM from the result of function calls. Therefore, when we use memoization for React components using React.memo, React can prevent re-renders when they’re not needed by returning the output of functional components from the memoization cache rather than re-computing unnecessarily.

How does memoization affect performance?

Have you ever interacted with an application only to find that you can’t accomplish the task you were working on because the UI completely freezes when you interact with it?

To illustrate this, let’s talk about a real-life example. You’re on your favorite shoe e-commerce site and you find that perfect grail pair of limited edition shoes. Your heart leaps out of your chest at your excitement that you’ll have a fresh new pair of kicks, so you hit the checkout button and grab your credit card.

As you’re typing your credit card details into the checkout form, all of a sudden the UI freezes every time you hit a key. During the time it takes you to complete checkout, someone else snags your coveted grail sneaks. Your day is ruined due to a UI that was likely not properly memoized!

In this day and age, we are used to highly responsive dynamic applications and it’s an extremely poor user experience to inadvertently interrupt your user by freezing the app. This type of problem will affect user experience and most likely will result in a failed product.

In most cases, React is performant enough that your applications should be very fast. However, in some specific cases, you can run into the problem discussed above. This is exactly the type of issue that memoization can help solve. Hang with us and we’ll explore this in detail below.

When should you use memoization?

This question raises an interesting debate, one that we have discussed at length at basically every company I’ve ever worked for.

On one hand, some engineers feel that you should over-use memoization, some even going as far to say that you should use memoization on everything.

On the other hand, some engineers argue that you should use memoization sparingly and will often bring up the famous Donald Knuth quote that “premature optimization is the root of all evil”.

Let’s take a look below at the pros and cons of each side of the debate.

Over utilize memoization

Pros

  • Knowing when memoization is needed is nuanced leading to expensive team conversations & code reviews with minimal value.

Cons

  • There is a small performance hit for memoizing functions that don’t need it.
  • Added overhead to code legibility of adding memoization everywhere.

Use memoization sparingly

Pros

  • Encourages deep understanding of React’s performance lifecycle and memoization tools.

Cons

  • Potential to miss a performance issue, leading to negative user experience.
  • It’s a complex topic and is difficult to master, creating slowdowns to developer productivity.

Our view

It’s our view that the right answer is somewhere in the middle of these two extremes. We believe that every engineer should understand React’s lifecycle and memoization tools deeply and should be thoughtful and deliberate about when to use them. When in doubt, our recommendation is to favor the side of caution because there is not a lot of downside to over-using memoization and it can prevent fairly severe and elusive user experience issues.

High-level considerations

Before we get into specifics about the various tools available in React and how they are used, it’s important to take a high-level look at where you might consider using memoization.

One of the most common use cases for memoization is when you map over an array of data that you pass to a ChildComponent.

For example:

{data.map(item => ( <ChildComponent key={item.id} {...item} /> ))}

If you find yourself wondering whether you need memoization for this example, ask yourself the following:

How often will a re-render be triggered?

  1. For example, a form will re-render on every key stroke, whereas a simple list may not re-render often or at all.

Is the data static or dynamic at runtime?

  1. There may be cases where you are mapping over a short static array where memoization would be overkill.
  2. On the other hand, if you are loading data from an API and the complexity of the data structure is not known at runtime you may want to use memoization to be safe.

How complex is the ChildComponent?

  1. Is your ChildComponent simple enough that frequent re-renders would not cause a performance problem?
  2. For example, a component that renders a simple label probably doesn’t need memoization.
  3. Although as we discussed previously, it might not hurt to use it anyways.

Is ChildComponent passing props to React's lifecycle hooks, such as useEffect?

  1. Passing an object to the dependency array of a useEffect will trigger an update on every render.
  2. If you are making use of a lot of lifecycle hooks, you may inadvertently be causing extra re-renders and it might be a good use case for memoization.

The complexity is there is no clear algorithm to decide when to use memoization, it’s a judgement call considering all of the above. From there, you need to understand the available tools that React provides, which we’ll go into below.

React’s memoization APIs

React.memo to the rescue

React’s memo utility allows a developer to control when a component will re-render based on its props. It's a direct replacement for the shouldComponentUpdate function and React.PureComponent, which were used in previous versions of React for the same purpose.

In many ways, it’s the most important memoization utility that React has to offer and it’s our opinion that it should be the first line of defense in terms of solving memoization issues. Whenever you create a new component, you should consider whether memoization is needed.

Several points to consider before using React.memo are as follows:

  1. Is the component significantly complex where repeated re-renders would cause a performance hit?
  2. Will it be used many times at once, for example as a return from a .map?
  3. A single use AppBar component may not need to be memoized, but a re-usable Card component probably should.
  4. Is it a pure functional component? Most modern React developers do not use Class components anymore, but if you do you would make use of shouldComponentUpdate instead. See: the React docs on the subject for more info.

When in doubt, you are unlikely to hurt performance by using the default React.memo for your reusable components, but you should be clear on how it works.

How to use React.memo

Checkout the docs for more info on how to use React.memo effectively.

React.memo is a Higher Order Component (HOC) that you need to wrap around your component. By default, it will do shallow comparison of the component’s props.

For example, below is the most simple usage of React.memo:

// your React component
const MyComponentBase = () => (// some JSX)
export const MyComponent = memo(MyComponentBase)// The component will show as `<anonymous>` in the React Dev Tools
// unless you set a displayName:
MyComponent.displayName = 'MyComponent'

Shallow comparison means that for non-primitive props such as objects, functions, and arrays, it will check referential equality using strict comparison (===) and will not compare the values nested within them. This is extremely important to understand to use React.memo correctly, otherwise you may unintentionally be causing re-renders when you don’t expect it.

For example, in our memoization demo app, let’s say that we just memoized the MemoizedBrokerageCard by default without customization:

export const MemoizedBrokerageCard = memo(MemoizedBrokerageCardBase);
MemoizedBrokerageCard.displayName = 'MemoizedBrokerageCard';

Within our screen we render the MemoizedBrokerageCard component like so:

export function Memoized() {
const [brokerages, setBrokerages] = useState<any>([]);

const handleUpdate = useCallback((id: string, value: string) => {
setBrokerages(
brokerages.map(
(brokerage: BrokerageType) => brokerage.id === id
? { ...brokerage, catchPhrase: value }
: brokerage
)
);
}, [brokerages]);
return (
// other components
<Grid>
{brokerages.map((brokerage: BrokerageType) => (
<MemoizedBrokerageCard
key={brokerage.id}
onUpdate={handleUpdate}
{...brokerage}
/>
))}
</Grid>
);
}

You might think that the example above would work as expected because we are using React.memo and are wrapping the handleUpdate callback with useCallback. Take a closer look and see if you can spot the problem!

The issue is that we are using the brokerages state within the useCallback as well as calling setBrokerages when it’s called. Because we call setBrokerages within the callback, the handleUpdate function will be re-created every single time it’s called, causing the MemoizedBrokerageCard to re-render on every keystroke despite our best efforts.

One simple way to solve this is to ignore the comparison to the onUpdate prop and do strict comparison of the rest of the props. You can do this by passing a comparison function as the second argument of React.memo. This is an example where understanding the tools and being able to use them to spot issues is essential.

Below is an example of the customized comparison function that fixed the memoization of MemoizedBrokerageCard:

export const MemoizedBrokerageCard = memo(
MemoizedBrokerageCardBase, (prev, next) => {
return prev.catchPhrase === next.catchPhrase
&& prev.contacts === next.contacts
&& prev.id === next.id
&& prev.name === next.name;
}
);
MemoizedBrokerageCard.displayName = 'MemoizedBrokerageCard';

Usage of React.useCallback

Anonymous functions that are passed as props to child components will update on every render and therefore should be wrapped in useCallback, especially if the child component is using React.memo.

export const FilterByField: FunctionComponent = () => {
const { filterBy, setFilterBy } = useOrdersFilterContext()

// ✅ useCallback for anonymous functions that are passed as props to children
const handleFilter = useCallback(
(e) => {
setFilterBy(e.target.value)
},
[setFilterBy]
)
return (
<SelectionTextField
label="Filter by"
value={filterBy}
multiple
onChange={handleFilter}
>
{/* Removed for brevity */}
</SelectionTextField>
)
}

It is not necessary to utilize useCallback for every single function that you pass to a child component, but you should think carefully about it.

Potential use cases for useCallback:

  1. When it’s a callback that you pass to a memoized component and you haven’t customized the memo comparison function.
  2. When it’s a return value from a hook or other shared function, just to be a good citizen.
  3. When it’s passed into a dependency of a lifecycle hook like useEffect.

Usage of React.useMemo

useMemo can be used for expensive computations and when an array or object is being updated frequently.

Potential use cases for useMemo:

  • Non-primitive values such as objects and arrays should be wrapped with useMemo when they are:
  • Passed in as dependencies to lifecycle hooks like useEffect.
  • Dynamic objects and arrays where you are not sure what size they will be at runtime.
  • Involved in an expensive computation, for example computing data for a visualization / chart, client-side machine-learning, image processing, etc.
  • Return values from re-usable hooks should often be memoized since you do not know how that hook is going to be used.

More Resources

Below are some great articles that explain the topic deeper and provide a selection of varying opinions on the subject. We highly recommend that you read these articles and come to your own conclusions about how to use memoization. As always, please provide us with feedback and your thoughts in the comments!

Profiling Tools

Articles

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ryan C. Collins

Ryan C. Collins

624 Followers

Hey, I’m Ryan! I work in the tech industry in leadership at Doma, and post content about tech, leadership, and personal finance. https://www.ryanccollins.com