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.