React Performance Optimization Tips
Tags: react, javascript, performance, webdev
Published: 2020-12-27 03:45:02

In this post, we will look into how we can improve the performance of React apps that need to render a lot of components on the screen.

We generally consider using `pagination` or `virtualization` for most of our apps to provide a better user experience and that works for most of the use cases, but what if we have a use case where we need to render a lot of components on the screen while not giving up on the user experience and performance. 

For the demonstration, I have considered a simple app that renders 30k squares on the screen, and we update the count when the squares are clicked. I am using `react 17.0.0` and functional components with hooks.

Here is the preview of the app. It has an `App` component and a `Square` component.  There is a noticeable lag on clicking the squares.

[Stackblitz Preview](https://react-perf-start-new.stackblitz.io/)
[Stackblitz Code](https://stackblitz.com/edit/react-perf-start-new?file=src/App.js)

```js
// App.jsx
import React, { useState } from "react";

import Square from "./components/square/square";

const data = Array(30000)
  .fill()
  .map((val, index) => {
    return { id: index, key: `square-${index}` };
  });

const App = () => {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(data);

  return (
    <div>
      <p>Count: {count}</p>
      {items.map(({ key, id, clicked }) => (
        <Square
          key={key}
          id={id}
          clicked={clicked}
          onClick={id => {
            const newItems = [...items];

            newItems[id].clicked = true;

            setCount(val => val + 1);
            setItems(newItems);
          }}
        />
      ))}
    </div>
  );
};

export default App;
```

```js
// Square.jsx
import React from "react";

import "./square.css";

const Square = ({ onClick, id, clicked }) => {
  return (
    <div
      className={`square ${clicked && "clicked"}`}
      onClick={() => onClick(id)}
    />
  );
};

export default Square;
```

Let's add console statements to both the components to check if they are rendering unnecessarily and then click on one of the squares. We see the `Square` component function is getting called 30k times.

Also, we can see that `600ms` are spent in re-rendering the UI on React Dev tools Profiler Tab. Start the profiling on page load -> click any square -> stop profiling.

![Profile Screenshot showing 600ms spent for re-rendering](https://dev-to-uploads.s3.amazonaws.com/i/8qfm4q7biwo5xp0s0jh9.png)

We need to avoid re-rendering of `Square` component as none of the `props` for it is changing. We will use `React.memo` for this.

## What is `React.memo` ?   
`React.memo` is a higher order component that helps to skip the re-rendering by [memoizing] (https://en.wikipedia.org/wiki/Memoization) the result of the initial render. `React.memo` re-renders component only when the `prop` changes.

> NOTE: `React.memo` does a shallow comparison. For more control, we can pass a comparison function as below.

> ```js
  React.memo(Component, (prevProps, nextProps) => {
    // return true if the props are same, this will skip re-render
    // return false if the props have changed, will re-render
  });
```
Here is the `Square` component with `React.memo`

```js
// Square component with React.memo
import React from "react";

import "./square.css";

const Square = ({ onClick, id, clicked }) => {
  return (
    <div
      className={`square ${clicked && "clicked"}`}
      onClick={() => onClick(id)}
    />
  );
};

export default React.memo(Square);
```

Now let's try to profile again with an additional setting as shown below. 

![Profile Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/bbwc5zjvu0btb83p5abi.png)

![Profile Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/vqakyyn9sl9snycu3ldv.png)

We don't see any difference yet. But when we hover on the `Square` component it shows `onClick` prop has changed which has triggered this re-render. This happens as we are passing a new function during each render for the `onClick` prop. To avoid this we use `useCallback`.
 
## What is `useCallback` ?

`useCallback` is a hook that returns a memoized callback.

```js
// App component with useCallback
import React, { useState, useCallback } from "react";

import Square from "./components/square/square";

const data = Array(30000)
  .fill()
  .map((val, index) => {
    return { id: index, key: `square-${index}` };
  });

const App = () => {
  const [count, setCount] = useState(0);
  const [items, setItems] = useState(data);

  const onClick = useCallback(
    id => {
      const newItems = [...items];
      newItems[id].clicked = true;
      setCount(val => val + 1);
      setItems(newItems);
    },
    [items]
  );

  return (
    <div>
      <p>Count: {count}</p>
      {items.map(({ key, id, clicked }) => (
        <Square key={key} id={id} clicked={clicked} onClick={onClick} />
      ))}
    </div>
  );
};

export default App;
```
Let's profile again. We are now avoiding re-rendering of `Squares` and this reduces the time to `118ms`.

![Profile Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/j0q4inaq5rvysnthme0d.png)

We see much better performance now. We are avoiding the re-rendering of `Square` components using memoization but `React` still needs to compare the props for all the 30k elements. Here is the component tree for our app.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ewaawskx9wsapdslfjpv.png)

If you still find performance issues we can go one step further. We have 30k `Square` elements below the `App` component. To reduce the time React takes to compare props we need to reduce the components at this level. What can be done here? Can we introduce another layer of components? Yes, we will be splitting the list of 30k items into smaller chunks and render those by using an intermediate component.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/152ge66ipvq7cfofo6tx.png)

In a real-world app, we can find a logical place to split the list into smaller chunks. But here let's split them into chunks of 500 Squares each.

```js
// App component
import React, { useState, useCallback } from "react";

import Row from "./components/row/row";

let num = 0;

const data = Array(30000)
  .fill()
  .map((val, index) => {
    if (index % 500 === 0) {
      num = 0;
    }
    return { id: num++, key: `square-${index}` };
  });

const chunkArray = (array, chunkSize) => {
  const results = [];
  let index = 1;

  while (array.length) {
    results.push({
      items: array.splice(0, chunkSize),
      key: String(index)
    });
    index++;
  }

  return results;
};

const chunks = chunkArray(data, 500);

const App = () => {
  const [count, setCount] = useState(0);
  const [allItems, setAllItems] = useState(chunks);

  const onClick = useCallback(
    (id, index) => {
      const chunk = [...allItems[index].items];
      chunk[id].clicked = true;
      setCount(val => val + 1);
      allItems[index].items = chunk;
      setAllItems(allItems);
    },
    [allItems]
  );

  return (
    <div>
      <p>Count: {count}</p>
      {allItems.map(({ items, key }, index) => (
        <Row items={items} onClick={onClick} key={key} index={index} />
      ))}
    </div>
  );
};

export default App;
```

```js
// Row component
import React, { useCallback } from "react";

import Square from "../square/square";

const Row = ({ items, onClick, index }) => {
  const onItemClick = useCallback(
    id => {
      onClick(id, index);
    },
    [onClick, index]
  );

  return (
    <>
      {items.map(({ id, key, clicked }) => (
        <Square key={key} onClick={onItemClick} id={id} clicked={clicked} />
      ))}
    </>
  );
};

export default React.memo(Row);
```

Let's profile again. We do not see any lag now. We have a lot fewer `Row` components so the prop comparison is pretty quick also React can skip `Square` prop comparison if the `Row` props have not changed.

![Profile Screenshot](https://dev-to-uploads.s3.amazonaws.com/i/25ml6ed53b5v04mff0ny.png)

Here is the final app
[Stackblitz Preview](https://react-perf-final-new.stackblitz.io/)
[Stackblitz Code](https://stackblitz.com/edit/react-perf-final-new?file=src/App.js)

`React.memo` and `useCallback` can be used to get better performance. Does it mean we should wrap all components with `React.memo` and all functions with `useCallback`? **No**. `React.memo` and `useCallback` use memoization which adds up to the memory, also the functions themselves take time to run and have overheads like the prop comparison. The splitting that we have done adds up to the memory as well.

## When to use `React.memo` and `useCallback`?
They are not required unless you see some lag in a specific component or the complete app. If there is a lag try profiling for the actions on that screen and check if there can be any component re-renders that can be avoided. `useCallback` is also useful in cases where we are using the functions as dependencies for hooks to avoid unnecessary code blocks to run.

## Conclusion
While `React.memo`, `useCallback`, `useMemo` can be used to optimize the performance of the React apps they are not required in most cases. Use them Cautiously.