Faster Renders with React.Memo

memo it

This post I'm getting into React.memo and how it can be used to speed up a React app. I'll start from an example app that contains some unoptimized React code. Using the React Devtools Profiler my goal is to demonstrate how you could go about fine tuning a React app to be be as performant as it can be. Using this approach will allow you to step through each part of your React app and increase performance where you see fit.

Example App

This app is a big list of items that belong to subgroups and those subgroups belong to parent groups which are labeled as A, B, C, etc. There are 15 items in each subgroup and 2 subgroups per parent group bring the grand total of items to 780. That's a lot.

Functionality wise, all we want to do is make selections within this list of items. On click of an item that item should be highlighted in red.

Baseline App Performance

Time to get a baseline for how the app is currently performing. Using the React Devtools Profiler we can do some measuring.

Above is the profiler that shows where the app is spending time. Each one of the blocks is a React component. The colors range from blue to orange. Blue means fast, orange means slow.

Above you can also see that every component is doing something whenever we make an update to a single component. We just want to update the single component we are acting on or the component path to that individual component.

Lastly, on the right you see a metric called the Render duration. The first item we tried to update took 55.5ms for React to commit that change to the DOM.

React.memo

In the example React is processing all of the components and selecting a single item within the list which means that in this case, 779 items are staying the exact same. React exposes React.memo for this kind of use case.

import React from "react";

function Item({ item, selectItem, group, subgroup }) {
  return (
    <div
      className={`item${item.selected ? " item--selected" : ""}`}
      onClick={() => selectItem(group, subgroup, item.id)}
    >
      <div className="item-img">
        <img src="https://placehold.it/64x64" />
      </div>
      <span>{item.name}</span>
    </div>
  );
}

const MemoItem = React.memo(Item, (prevProps, nextProps) => {
  if (prevProps.item.selected === nextProps.item.selected) {
    return true;
  }
  return false;
});

export default MemoItem;

Above is the Item component. At the bottom it's been updated to export a memoized version of the Item component. The function signature looks like this React.memo(MyReactComponent, compareFn(prevProps, nextProps)). Within the compare function you tell React if the component is the same(true) or not(false).

Time to measure.

What changed? The render duration is now at 14.7ms 👍. Diving further into the components you can see all the Item components are now grey except for the one where the change was made. That's closer to the goal!

Unfortunately, there is still extra processing being done that is not necessary. Each one of the ItemGroup components are still doing work when only one of them has been updated. Back to React.memo.

import React from "react";
import Item from "./Item";

function ItemGroup({ items, selectItem, group }) {
  return (
    <div className="item-group">
      <div className="item-group--a">
        {items.SUB_GROUPA.items.map(item => (
          <Item
            group={group}
            subgroup={"SUB_GROUPA"}
            key={`item-${item.id}`}
            item={item}
            selectItem={selectItem}
          />
        ))}
      </div>
      <div className="item-group--b">
        {items.SUB_GROUPB.items.map(item => (
          <Item
            group={group}
            subgroup={"SUB_GROUPB"}
            key={`item-${item.id}`}
            item={item}
            selectItem={selectItem}
          />
        ))}
      </div>
    </div>
  );
}

const MemoItemGroup = React.memo(ItemGroup, (prevProps, nextProps) => {
  if (prevProps.updatedAt === nextProps.updatedAt) {
    return true;
  }
  return false;
});

export default MemoItemGroup;

Above contains the ItemGroup component which processes each group of items. The default export is now the memoized version of ItemGroup which contains a comparision between the updatedAt timestamp variables.

With this last change the render duration is now 4.3ms! 🚀

Thoughts

Any sort of comparison can be done against the props. I try to use booleans and numbers(timestamp) as they seem simpler to process versus comparing objects.

Another important part in all of this is correctly using keys on collections of items. I found out the hard way how important this is. So make sure those are set properly.

Lastly, the React Devtools Profiler is a great tool for getting a better understanding of your React app. Often we are forced to hit deadlines and cut performance corners. If you can, take a few days and try and deeply understand the parts of your React app to try and figure out where to create fast paths for rendering.


If you liked this article and want to say hi, then you can find me on Twitter.