Performance optimization means improving a React application's speed, responsiveness, and efficiency. React already provides an efficient rendering system, but large applications may need additional techniques to reduce unnecessary work.
React performance describes how efficiently a React application renders, updates, and responds to user interactions.
Good performance can result in:
Performance problems can come from different parts of an application.
A component may render again when its state or relevant inputs change. Parent component updates can also cause child components to render again.
function Counter() {
const [count, setCount] =
useState(0);
return (
<button
onClick={() =>
setCount(count + 1)
}
>
{count}
</button>
);
}
One important performance principle is to avoid doing expensive work when it is not needed.
const result =
expensiveCalculation(data);
If the calculation is expensive and the required inputs rarely change, memoization may sometimes be useful.
React.memo can memoize a component so React can skip rendering it
when its props have not changed according to the memoization comparison.
const UserCard =
React.memo(function UserCard({
name
}) {
return (
<div>
{name}
</div>
);
});
React.memo can be useful when:
It is not automatically beneficial for every component.
useMemo can cache the result of a calculation between renders
until its dependencies change.
const filteredUsers =
useMemo(() => {
return users.filter(
user =>
user.active
);
}, [users]);
An expensive calculation may involve processing a large amount of data.
function calculateTotal(items) {
return items.reduce(
(total, item) =>
total + item.price,
0
);
}
If the calculation is expensive and does not need to run on every render, memoization can be considered.
useCallback can preserve a function reference between renders
until its dependencies change.
const handleClick =
useCallback(() => {
console.log("Clicked");
}, []);
This can be useful when passing callbacks to memoized child components.
| Hook | Purpose |
|---|---|
| useMemo | Memoizes a calculated value |
| useCallback | Memoizes a function reference |
useMemo(
() => calculateValue(),
[data]
);
useCallback(
() => handleAction(),
[data]
);
An unnecessary useEffect can make code more complicated and
cause additional updates.
useEffect(() => {
setFullName(
firstName + " " + lastName
);
}, [firstName, lastName]);
If fullName is simply derived from existing state or props, it
may often be calculated directly during rendering instead.
Derived data can often be calculated from existing state instead of storing duplicate state.
const fullName =
firstName + " " + lastName;
Keeping only the necessary state can make the component easier to understand and reduce synchronization work.
Large lists can require significant rendering work.
{users.map(user => (
<UserCard
key={user.id}
user={user}
/>
))}
Use stable keys and consider additional techniques when displaying very large lists.
For extremely large lists, rendering every item at once can be expensive. Virtualization renders only the items currently needed for the visible area.
Large List
↓
Visible Items
↓
Render Only Needed Items
Libraries designed for list virtualization can be used when an application has a suitable large-list requirement.
Pagination can reduce the amount of data displayed at one time.
Page 1
Page 2
Page 3
Page 4
Instead of rendering thousands of records at once, the application can display a smaller group of records.
Lazy loading allows parts of an application to be loaded when they are needed instead of loading everything immediately.
const Dashboard =
lazy(() =>
import("./Dashboard")
);
Lazy loading is commonly used with route-level code splitting.
React.lazy() can dynamically load a component.
import {
lazy,
Suspense
} from "react";
const About =
lazy(() =>
import("./About")
);
function App() {
return (
<Suspense
fallback={
<p>
Loading...
</p>
}
>
<About />
</Suspense>
);
}
Code splitting divides application JavaScript into smaller chunks that can be loaded when required.
Application
↓
┌────┼────┐
↓ ↓ ↓
Home About Dashboard
This can reduce the amount of JavaScript required for the initial page load.
Large images can increase page loading time.
Useful practices include:
<img
src="/images/course.webp"
alt="React Course"
loading="lazy"
/>
Unnecessary API requests can slow down an application.
Consider:
fetch("/api/products?page=1")
.then(response =>
response.json()
)
.then(data =>
setProducts(data)
);
When a user types into a search box, sending an API request for every keystroke may create unnecessary requests.
User types:
R
Re
Rea
Reac
React
Instead of requesting every time,
wait briefly before sending the request.
This technique is commonly called debouncing.
Creating new objects or functions during every render can affect referential equality and may matter when working with memoized components.
const options = {
sort: "name"
};
return (
<Child
options={options}
/>
);
If object identity causes unnecessary child renders, the structure can be reviewed and, where appropriate, memoization can be considered.
Breaking a large component into smaller components can improve code organization and make performance behavior easier to reason about.
Dashboard
├── Header
├── Sidebar
├── Statistics
├── UserList
└── Footer
Component splitting should be based on meaningful responsibilities rather than creating unnecessary tiny components.
The React Developer Tools Profiler can help identify components that render and how long rendering takes.
Performance Problem
↓
Profile Application
↓
Find Expensive Render
↓
Identify Cause
↓
Apply Optimization
↓
Measure Again
Profiling helps prevent guessing about performance problems.
Premature optimization means adding optimization techniques before there is evidence that they are needed.
For example, adding useMemo to every calculation may make code
more complicated without providing a meaningful benefit.
Build Application
↓
Measure Performance
↓
Find Bottleneck
↓
Understand Cause
↓
Apply Suitable Optimization
↓
Measure Again
↓
Keep or Remove Optimization
import {
useMemo,
useState,
memo
} from "react";
const UserCard =
memo(function UserCard({
user
}) {
return (
<div className="p-3 border mb-2">
<strong>
{user.name}
</strong>
</div>
);
});
function Users() {
const [users] = useState([
{
id: 1,
name: "Amit"
},
{
id: 2,
name: "Ravi"
},
{
id: 3,
name: "Neha"
}
]);
const [search, setSearch] =
useState("");
const filteredUsers =
useMemo(() => {
return users.filter(
user =>
user.name
.toLowerCase()
.includes(
search.toLowerCase()
)
);
}, [users, search]);
return (
<div>
<input
value={search}
onChange={e =>
setSearch(
e.target.value
)
}
placeholder="Search users"
/>
{filteredUsers.map(
user => (
<UserCard
key={user.id}
user={user}
/>
))}
</div>
);
}
export default Users;
React.memo where it provides a meaningful benefit.useMemo for appropriate expensive calculations.useCallback when stable function references are useful.Measure
↓
Find Problem
↓
Optimize
↓
Measure Again
React.memo can skip some unnecessary component renders.useMemo memoizes a calculated value.useCallback memoizes a function reference.Question: Which React hook is used to memoize the result of a calculation?