Optimizing React performance by preventing unnecessary re-renders

12 Feb 2021

Re-rendering React components unnecessarily can slow down your app and make the UI feel unresponsive.

This article explains how to update components only when necessary, and how to avoid common causes of unintentional re-renders.

Use React.memo or React.PureComponent

When a component re-renders, React will also re-render child components by default.

Here's a simple app with two Counter components and a button that increments one of them.

Simple app using React.memo

function App() {
  const [counterA, setCounterA] = React.useState(0);
  const [counterB, setCounterB] = React.useState(0);

  return (
    <div>
      <Counter name="A" value={counterA} />
      <Counter name="B" value={counterB} />
      <button
        onClick={() => {
          console.log("Click button");
          setCounterA(counterA + 1);
        }}
      >
        Increment counter A
      </button>
    </div>
  );
}
function Counter({ name, value }) {
  console.log(`Rendering counter ${name}`);
  return (
    <div>
      {name}: {value}
    </div>
  );
}

Right now, both Counter components render when the button is clicked, even though only counter A has changed.

Click button
Rendering counter A
Rendering counter B

The React.memo higher-order component (HOC) can ensure a component is only re-rendered when its props change.

const Counter = React.memo(function Counter({ name, value }) {
  console.log(`Rendering counter ${name}`);
  return (
    <div>
      {name}: {value}
    </div>
  );
});

Now only counter A is re-rendered, because it's value prop changed from 0 to 1.

Click button
Rendering counter A

For class-based components

If you're using class-based components instead of function components, change extends React.Component to extends React.PureComponent to get the same effect.

Make sure property values don't change

Preventing the render in our example was pretty easy. But in practice this is more difficult, as it's easy for unintentional prop changes to sneak in.

Let's include the Increment button in the Counter component.

React.memo demo with callback prop

const Counter = React.memo(function Counter({ name, value, onClickIncrement }) {
  console.log(`Rendering counter ${name}`);
  return (
    <div>
      {name}: {value} <button onClick={onClickIncrement}>Increment</button>
    </div>
  );
});

The App component now passes in an onClickIncrement prop to each Counter.

<Counter
  name="A"
  value={counterA}
  onClickIncrement={() => setCounterA(counterA + 1)}
/>

If you increment counter A, both counters are re-rendered.

Rendering counter A
Rendering counter B

Why? Because the value of the onClickIncrement prop changes every time the app re-renders. Each function is a distinct JavaScript object, so React sees the prop change and makes sure to update the Counter.

This makes sense, because the onClickIncrement function depends on the counterA value from its parent scope. If the same function was passed into the Counter every time, then the increment would stop working as the initial counter value would never update. The counter value would be set to 0 + 1 = 1 every time.

The problem is that the onClickIncrement function changes every time, even if the counter value it references hasn't changed.

We can use the useCallback hook to fix this. useCallback memoizes the function that's passed in, so that a new function is only returned when one of the hook dependencies changes.

In this case the dependency is the counterA state. When this changes, the onClickIncrement function has to update, so that we don't use outdated state later on.

<Counter
  name="A"
  value={counterA}
  onClickIncrement={React.useCallback(() => setCounterA(counterA + 1), [
    counterA,
  ])}
/>

If we increment counter A now, only counter A re-renders.

Rendering counter A

For class-based components

If you're using class-based components, add methods to the class and use the bind function in the constructor to ensure it has access to the component instance.

constructor(props) {
  super(props)
  this.onClickIncrementA = this.onClickIncrementA.bind(this)
}

(You can't call bind in the render function, as it returns a new function object and would cause a re-render.)

Passing objects as props

Unintentional re-renders not only happen with functions, but also with object literals.

function App() {
  return <Heading style={{ color: "blue" }}>Hello world</Heading>
}

Every time the App component renders a new style object is created, leading the memoized Heading component to update.

Luckily, in this case the style object is always the same, so we can just create it once outside the App component and then re-use it for every render.

const headingStyle = { color: "blue" }
function App() {
  return <Heading style={headingStyle}>Hello world</Heading>
}

But what if the style is calculated dynamically? In that case you can use the useMemo hook to limit when the object is updated.

function App({ count }) {
   const headingStyle = React.useMemo(
    () => ({
      color: count < 10 ? "blue" : "red",
    }),
    [count < 10]
  );
  return <Heading style={headingStyle}>Hello world</Heading>
}

Note that the hook dependency is not the plain count, but the count < 10 condition. That way, if the count changes, the heading is only re-rendered if the color would change as well.

children props

We get the same problems with object identity and unintentional re-renders if the children we pass in are more than just a simple string.

<Heading>
  <strong>Hello world</strong>
</Heading>

However, the same solutions apply. If the children are static, move them out of the function. If they depend on state, use useMemo.

function App({}) {
  const content = React.useMemo(() => <strong>Hello world ({count}</strong>, [
    count,
  ]);

  return (
    <>
      <Heading>{content}</Heading>
    </>
  );
}

Using keys to avoid re-renders

Key props allow React to identify elements across renders. They're most commonly used when rendering a list of items.

If each list element has a consistent key, React can avoid re-rendering components even when list items are added or removed.

Toggle container demo

function App() {
  console.log("Render App");
  const [items, setItems] = React.useState([{ name: "A" }, { name: "B" }]);
  return (
    <div>
      {items.map((item) => (
        <ListItem item={item} />
      ))}
      <button onClick={() => setItems(items.slice().reverse())}>Reverse</button>
    </div>
  );
}
const ListItem = React.memo(function ListItem({ item }) {
  console.log(`Render ${item.name}`);
  return <div>{item.name}</div>;
});

Without the key on <ListItem> we're getting a Warning: Each child in a list should have a unique "key" prop message.

This is the log output when clicking on the Reverse button.

=> Reverse
Render app
Render B
Render A

Instead of moving the elements around, React instead updates both of them and passes in the new item prop.

Adding a unique key to each list item fixes the issue.

<ListItem item={item} key={item.name} />

React can now correctly recognize that the items haven't changed, and just moves the existing elements around.

What's a good key?

Keys should be unique, and no two elements in a list should have the same key. The item.name key we used above isn't ideal because of this, as multiple list elements might have the same name. Where possible, assign a unique ID to each list item – often you'll get this from the backend database.

Keys should also be stable. If you use Math.random() then the key will change every time, causing the component to re-mount and re-render.

For static lists, where no items are added or removed, using the array index is also fine.

Keys on fragments

You can't add keys to fragments using the short syntax (<>), but it works if you use the full name:

<React.Fragment key={item.name}>
</React.Fragment>

Avoid changes in the DOM tree structure

Child components will be remounted if the surrounding DOM structure changes. For example, this app adds a container around the list. In a more realistic app you might put items in different groups based on a setting.

Toggle container demo

function App() {
  console.log("Render App");
  const [items, setItems] = React.useState([{ name: "A" }, { name: "B" }]);
  const [showContainer, setShowContainer] = React.useState(false);
  const els = items.map((item) => <ListItem item={item} key={item.name} />);
  return (
    <div>
      {showContainer > 0 ? <div>{els}</div> : els}
      <button onClick={() => setShowContainer(!showContainer)}>
        Toggle container
      </button>
    </div>
  );
}
const ListItem = React.memo(function ListItem({ item }) {
  console.log(`Render ${item.name}`);
  return <div>{item.name}</div>;
});

When the parent component is added all existing list items are unmounted and new component instances are created. React Developer Tools shows that this is the first render of the component.

React Developer Tools update because of first render

Where possible, keep the DOM structure the same. For example, if you need to show dividers between groups within the list, insert the dividers between the list elements, instead of adding a wrapper div to each group.

DebugBear is a website monitoring tool built for front-end teams. Track performance metrics and Lighthouse scores in CI and production. Learn more.

Get new articles on web performance by email.

DebugBear logo
Track and analyze site speed with DebugBear.
➔ Learn more