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

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

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);

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.

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.

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.

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