React Memoization Cheatsheet: 5 different ways to memoize and why it matters

React is improving fast and new versions pack a lot of different cool features built into the library. One of the coolest is the set of tooling around Memoization. In this post we'll learn a few tips and tricks on the subject, things I've used in production and helped me speed up heavy renders.

Why it matters

Already know why it matters? Go straight to the cheat sheet ⏭

Memoizing is a well-known concept in computer programming, aiming to speed up programs by caching results of expensive function calls and re-using those cached results as to avoid repeating those expensive operations:

Memoization graph showing expensive vs cached renders Memoization speeding up an expensive component's render

When using React, depending on how big and complex your component tree is, the process of rendering might be one of these expensive operations. The process of reconciliation alone might already be too heavy if it always has to go over the whole tree. This kind of computation, when done in the UI thread, can impose a heavy tax on the user experience, making your UI non-responsive and sluggish. In fact if we want to meet RAIL's goals and guidelines we have 50ms of time to do computations before we actually respond to user input.

A heavy, unresponsive and sluggish UI can lead to frustrated users and a sense of fatigue while using your application and on React application avoiding re-renders is usually one of the most impactful performance improvements you can do on Rendering Performance

The Cheat Sheet

This is the actual cheatsheet :) If you know some other ways to use memoization, lemme know!

Memoizing Components

When a component does not need to render when it's props change, we can let React know that so that it won't try to render from that component down unless it has to.

Note that the advantage of Memoizing shared.components only pays off when rendering is expensive. For simple children trees it can be quicker to just do the render compared to the overhead of comparing props.

Function Components

React comes with an awesome HoC: React.memo that allows us to memoize function shared.components:


// When we use the HoC without supplying a comparation function it
// will shallowly compare the props of the component to determine
// if it calls the render function
const MemoizedComponent = React.memo(MyComponent)

// We can supply a custom comparation function:
const areEqual = (prevProps, nextProps) => {
  // Only re-render when 'id' changes
  return prevProps.id === nextProps.id;
}

const MemoizedComponent = React.memo(MyComponent, areEqual);

Class Components

React has shipped with shouldComponentUpdate for a long time. shouldComponentUpdate is a method which is used in the reconciliation algorithm to tell if a component should trigger it's render() method or not. Like with React.memo there's an easy way to implement that comparison if we only need a default shallow comparison of props: React.PureComponent


// Shallow comparison of props on shouldComponentUpdate
class MyComponent extends React.PureComponent {
  render(props) {
    return <div />
  }
}

// Custom shouldComponentUpdate
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // Only re-render when 'id' prop changes
    // and state.active changed
    return this.props.id !== nextProps.id &&
           this.state.active !== nextState.active;
  }

  render(props) {
    return <div />
  }
}

Note that shouldComponentUpdate is the opposite of the comparison function for React.memo which can be interpreted as a areEqual function

Memoizing Props

When using literals, like string and number shallow comparison usually suffices for component memoization. However if we're using functions and objects, we need to memoize them in a different way. Because:


{ foo: 'bar' } === { foo: 'bar' } // false
[1, 2, 3] === [1, 2, 3] // false
(a => a) === (a => a) // false

Don't worry though, React ships with some cool helpers for that too :)

Memoizing Functions

To memoize functions, we can use useCallback


// Creating a memoized callback:
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Example of usage in a component

function MyComponent({ id, ...props }) {
  // Will only rerender Button if id changes
  const onButtonClick = useCallback(
    () => getUserData(id),
    [id],
  );

  return (
    // If Button is memoized, it will only rerender if id changed
    <Button onClick={onButtonClick}>I'm a button</Button>
  );
}

// if we had a <button onClick={() => getUserData(id)}/>
// the component would even if the id did not change

Memoizing heavy computations, Objects, Arrays, Sets, etc

To memoize the return of heavy computation functions, Objects, Arrays, Sets, etc we can use useMemo. We're meant to use this only if the value needs to change based on a certain property, if you need a consistent reference there is another way

Up until now, we were memoizing values to prevent the render from happening. With useMemo however, we can prevent heavy computation from happening as well by storing a value in memory.


// computeExpensiveValue only called if a or b change value
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

// object only changes if a or b changes
const memoizedObject = useMemo(() => ({ a, b }), [a, b]);

Example of usage in a component

function ProfilePage({
  profile: {
    name,
    phone,
  }
  visitedLocations,
  favoriteLocations,
}) {
  // Only changes if name or phone changes
  const userData = useMemo(() => ({
    name,
    phone: format(phone),
  }), [name, phone]);

  // Only gets called again if visitedLocations change
  const visited = useMemo(
    () => calculateClusters(visitedLocations),
  [visitedLocations]);

  // Only gets called again if favoriteLocations change
  const favorites = useMemo(
    () => calculateClusters(favoriteLocations),
  [favoriteLocations]);

  return (
    <>
      <Header />
      <Map clusters={visited} /> // If memoized, only rerender if visited changed
      <Divider />
      <Map clusters={favorites} /> // If memoized, will only rerender if favorites changed
      <Profile data={userData} /> // If memoized, will only rerender if userData changed
    <>
  );
}

Note that most of the time you shouldn't be doing heavy computation inside render methods, if possible, you should avoid doing that in the UI thread altogether. If you're interested, Surma has a really good article on how to move computation off of the main thread

Consistent Reference When Content Does Not Change

To get a consistent reference to a value, we can use the useRef hook:


const refContainer = useRef(initialValue);

// If you want to rename it and not use the .current
const { current: value } = useRef(initialValue);

Example of usage in a component (useRef)

function ProfileData({
  data,
  id,
}) {
  const intervalRef = useRef();

  useEffect(() => {
    const intervalId = setInterval(() => {
        updateData(id)
    });
    intervalRef.current = intervalId;
    return () => {
      clearInterval(intervalRef.current);
    };
  }, [id]);

  return (
    <>
      <div>{data}</div>
      // Button won't re-render on every interval tic
      <Button onClick={() => clearInterval(intervalRef.current)}>
        Stop updates
      </Button>
    <>
  );
}

We can use it to get a reference to initial prop values as well:


function TracedComponent({ trace, ...props}) {
  // keep reference to the initial values
  const { current: initialValues } = useRef({
      trace,
      params: {
        traceName: 'component',
        ...props
    }
  })

  useEffect(() => {
    const { trace, params } = initialValues;
    trace.start(params) // Only cares about initial values
  }, [])

  // Needs to be updated when props change 👇🏾
  return <Component {...props} />
}


See Also

  1. React Top Level API -- React;
  2. Hooks API Reference -- React;
  3. Hooks FAQ -- React;
  4. You’re overusing useMemo: Rethinking Hooks memoization -- LogRocket Blog;
  5. Measure Performance with the RAIL Model -- Web Fundamentals;
  6. Rendering Performance -- Web Fundamentals
  7. Use web workers to run JavaScript off the browser's main thread -- web.dev