React Performance Optimization Tips

harshdand

Harsh

Posted on December 27, 2020

React Performance Optimization Tips

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
Stackblitz Code

// 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;
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode

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

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 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.

  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

// 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);
Enter fullscreen mode Exit fullscreen mode

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

Profile Screenshot

Profile Screenshot

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.

// 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;
Enter fullscreen mode Exit fullscreen mode

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

Profile Screenshot

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

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

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.

// 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;
Enter fullscreen mode Exit fullscreen mode
// 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);
Enter fullscreen mode Exit fullscreen mode

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

Here is the final app
Stackblitz Preview
Stackblitz Code

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.

💖 💪 🙅 🚩
harshdand
Harsh

Posted on December 27, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related