React performance optimization - memoization demystified

Abhay V Ashokan

Abhay V Ashokan

June 13, 2023

When it comes to building fast React applications, performance is a top priority. Luckily, React has clever techniques built-in that take care of performance optimizations automatically. In fact, React does most of the heavy lifting for you, so you can focus on building your app without worrying too much about performance tweaks. However, as your React application scales and becomes more complex, there are opportunities to further enhance its speed and efficiency.

In this blog, we will focus on how the memoization of components and proper code splitting helps you squeeze the most out of your application. We assume that you have a high-level understanding of how useCallback, useMemo, and React.memo works. If so, let's directly jump right in.

Initial setup

We'll go through the process of building a website that helps teams manage and discuss customer feedback. The application contains a dashboard where different categories of feedback are organized. The user can easily navigate between each category and view all the feedback classified under it. It's important to note that for the purpose of this blog, we will be focusing on building a dashboard prototype rather than a fully functional application, incorporating a significant amount of demo data.

Customer feedback dashboard

The dashboard consists of mainly four components. The Header component displays the category of feedback and also lets you easily navigate to other categories. The Category component displays all the feedback related to the selected category. On the right side, the Info section provides personalized details about the user who is currently logged in. All these parts are encapsulated in the App component, creating a cohesive dashboard.

App.jsx

The App component renders the Header and Category components corresponding to the selected category. For the sake of demonstration, we store all the dummy data in the FEEDBACK_CATEGORIES variable. By default, the first category is selected. We shall pass a DEFAULT_USER constant to render the Info component.

import React, { useState } from "react";

import Category from "./Category";
import { DEFAULT_USER, FEEDBACK_CATEGORIES } from "./constants";
import Header from "./Header";
import Info from "./Info";

const App = () => {
  const [selectedCategoryIndex, setSelectedCategoryIndex] = useState(0);
  const totalCategories = FEEDBACK_CATEGORIES.length;
  const category = FEEDBACK_CATEGORIES[selectedCategoryIndex];

  const gotoNextCategory = () => {
    setSelectedCategoryIndex(index => (index + 1) % totalCategories);
  };

  const gotoPrevCategory = () => {
    setSelectedCategoryIndex(
      index => (index + totalCategories - 1) % totalCategories
    );
  };

  return (
    <div className="flex justify-between">
      <div className="w-full">
        <Header
          gotoNextCategory={gotoNextCategory}
          gotoPrevCategory={gotoPrevCategory}
          title={category.title}
        />
        <Category category={category} />
      </div>
      <Info user={DEFAULT_USER} />
    </div>
  );
};

export default App;

Header.jsx

The Header component renders the feedback title and implements pagination for seamless navigation between feedbacks.

import React from "react";

const Header = ({ title, gotoNextCategory, gotoPrevCategory }) => (
  <div className="flex justify-between p-4 shadow-sm">
    <h1 className="text-xl font-bold">{title}</h1>
    <div className="space-x-2">
      <button
        className="rounded bg-blue-500 py-1 px-2 text-white"
        onClick={gotoPrevCategory}
      >
        Previous
      </button>
      <button
        className="rounded bg-blue-500 py-1 px-2 text-white"
        onClick={gotoNextCategory}
      >
        Next
      </button>
    </div>
  </div>
);

export default Header;

Category.jsx

The Category component displays a list of feedbacks based on selected category, facilitating a bird's-eye view of all the feedbacks.

import React from "react";

const Category = ({ category }) => (
  <div className="mx-auto my-4 w-full max-w-xl space-y-4">
    {category.feedbacks.map(({ id, user, description }) => (
      <div key={id} className="rounded shadow px-6 py-4 w-full">
        <p className="font-semibold">{user}</p>
        <p className="text-gray-600">{description}</p>
      </div>
    ))}
  </div>
);

export default Category;

Info.jsx

The Info component displays the information of the currently logged in user.

import React from "react";

const Info = ({ user }) => (
  <div className="flex h-screen flex-col bg-gray-100 p-8">
    <p>{user.name}</p>
    <p className="font-semibold text-blue-500">{user.email}</p>
    <button className="text-end mt-auto block text-sm font-semibold text-red-500">
      Log out
    </button>
  </div>
);

export default Info;

Here is a CodeSandbox link for you to jump right in and try out all the changes yourselves.

Profiling and optimizing with React profiler

Now let's have a look at what the React Profiler has to say when we click on the "Next" and "Previous" buttons to navigate between the different feedbacks.

React profiler for initial code

Clearly, every component is re-rendered whenever the user navigates to another feedback. This is not ideal. We can argue that the Info component need not be re-rendered since the user information stays constant across every render. Let us wrap the default export of the Info component with React.memo and see how it improves the performance.

export default React.memo(Info);

React profiler for memoized Info component

It's now clear that subsequent renders use the cached version of the Info component boosting the overall performance.

Let's now explore how we can improve the performance of the Header component further. There is no point in memoizing the Header component itself since the title is updated whenever we navigate to a new category. We can see that the pagination buttons need not re-render whenever we navigate to a new category. Hence, it is possible to extract these buttons into their own component and memoize it to prevent these unnecessary re-renders.

Header.jsx

import React from "react";

import Pagination from "./Pagination";

const Header = ({ title, gotoNextCategory, gotoPrevCategory }) => (
  <div className="flex justify-between p-4 shadow-sm">
    <h1 className="text-xl font-bold">{title}</h1>
    <div className="space-x-2">
      <Pagination
        gotoNextCategory={gotoNextCategory}
        gotoPrevCategory={gotoPrevCategory}
      />
    </div>
  </div>
);

export default Header;

Pagination.jsx

import React from "react";

const Pagination = ({ gotoNextCategory, gotoPrevCategory }) => (
  <>
    <button
      className="rounded bg-blue-500 py-1 px-2 text-white"
      onClick={gotoPrevCategory}
    >
      Previous
    </button>
    <button
      className="rounded bg-blue-500 py-1 px-2 text-white"
      onClick={gotoNextCategory}
    >
      Next
    </button>
  </>
);

export default React.memo(Pagination);

This did not solve the problem. In fact, React.memo did not prevent any unnecessary re-renders. By definition, React.memo lets you skip re-rendering a component when its props are unchanged. After close inspection, you will understand that the references to the gotoNextCategory and gotoPrevCategory functions get updated whenever the App component re-renders. This causes the Pagination component to re-render as well. Here we should use the useCallback hook to cache the function references before passing them as props. This would maintain the referential equality of the functions across renders and let React.memo does its magic.

App.jsx

// Rest of the code

const App = () => {
  // Rest of the code

  const gotoNextCategory = useCallback(() => {
    setSelectedCategoryIndex(index => (index + 1) % totalCategories);
  }, []);

  const gotoPrevCategory = useCallback(() => {
    setSelectedCategoryIndex(
      index => (index + totalCategories - 1) % totalCategories
    );
  }, []);

  // Rest of the code
};

export default App;

Now, you may use the profiler to verify that the Pagination component is not re-rendered unnecessarily.

React profiler with memoized pagination

Let us now introduce a new feature. The users should be able to filter the feedback in a particular category based on a search term. We shall modify the Category component to incorporate it.

Feedback search feature

Category.jsx

import React, { useState } from "react";

const Category = ({ category }) => {
  const [searchTerm, setSearchTerm] = useState("");

  const filteredFeedbacks = category.feedbacks.filter(({ description }) =>
    description.toLowerCase().includes(searchTerm.toLowerCase().trim())
  );

  return (
    <div className="mx-auto my-4 w-full max-w-xl space-y-4">
      <input
        autoFocus
        className="outline-gray-200 w-full border p-2"
        placeholder="Search feedbacks"
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      {filteredFeedbacks.map(({ id, user, description }) => (
        <div key={id} className="rounded shadow px-6 py-4 w-full">
          <p className="font-semibold">{user}</p>
          <p className="text-gray-600">{description}</p>
        </div>
      ))}
    </div>
  );
};

export default Category;

From the above code, it is very clear that the individual feedback need not be re-rendered every time the search term is updated. Hence, it would be a good idea to extract it to a Card component and wrap it with React.memo.

Category.jsx

import React, { useState } from "react";

import Card from "./Card";

const Category = ({ category }) => {
  const [searchTerm, setSearchTerm] = useState("");

  const filteredFeedbacks = category.feedbacks.filter(({ description }) =>
    description.toLowerCase().includes(searchTerm.toLowerCase().trim())
  );

  return (
    <div className="mx-auto my-4 w-full max-w-xl space-y-4">
      <input
        autoFocus
        className="outline-gray-200 w-full border p-2"
        placeholder="Search feedbacks"
        value={searchTerm}
        onChange={e => setSearchTerm(e.target.value)}
      />
      {filteredFeedbacks.map(({ id, user, description }) => (
        <Card key={id} user={user} description={description} />
      ))}
    </div>
  );
};

export default Category;

Card.jsx

import React from "react";

const Card = ({ user, description }) => (
  <div className="w-full rounded px-6 py-4 shadow">
    <p className="font-semibold">{user}</p>
    <p className="text-gray-600">{description}</p>
  </div>
);

export default React.memo(Card);

If the number of feedbacks is too large, the calculation of filteredFeedbacks will become expensive. We need to traverse through all the comments one by one and then perform a custom search logic on each comment object. We can use the useMemo hook to cache the results preventing the same computation across renders boosts the performance further.

Let us verify the React profiler one last time. Clearly, only the Category component re-renders with the new changes, taking the rest of the results from the cache.

React profiler useMemo and memoized Card

To enhance the overall user experience, it's important to reset the search term whenever users navigate to a new category. This can be easily achieved by passing a key prop to the Category component. React will maintain separate component trees for each category, ensuring that the search functionality starts anew in each category.

// Rest of the code

const App = () => {
  // Rest of the code

  return (
    <div className="flex justify-between">
      <div className="w-full">
        <Header
          title={feedback.title}
          gotoNextCategory={gotoNextCategory}
          gotoPrevCategory={gotoPrevCategory}
        />
        <Category key={category.id} category={category} />
      </div>
      <Info user={DEFAULT_USER} />
    </div>
  );
};

export default App;

Wrapping up

By breaking down components and utilizing memoization, we have improved the performance of our app significantly. The React Profiler has been instrumental in identifying areas for optimization and validating the effectiveness of our enhancements. With these techniques, you can now build faster and more responsive React applications. Apply these learnings to your projects and elevate your React development skills.

Here is a CodeSandbox link of the optimized website for you to play with.

If this blog was helpful, check out our full blog archive.

Stay up to date with our blogs.

Subscribe to receive email notifications for new blog posts.