React Performance Optimization: A Practical Guide
Performance is one of the most important aspects of building production-grade React applications.
When an application becomes slow, developers often immediately reach for:
- useMemo()
- useCallback()
- React.memo()
But React performance optimization is much broader than memoization.
A better approach is to understand where the work is happening, measure the bottleneck, and then optimize the right layer.
In this article, we’ll explore the most important React performance optimization techniques with practical examples.
1. What Does React Performance Mean?
A simplified React rendering pipeline looks like this:
State / Props / Context Change
↓
React Render
↓
Reconciliation
↓
Commit Phase
↓
DOM Updates
↓
Browser Layout / Paint
↓
UIPerformance optimization means reducing unnecessary or expensive work in this pipeline.
There are several common sources of performance problems:
Unnecessary re-renders
+
Expensive calculations
+
Large component trees
+
Large JavaScript bundles
+
Huge lists
+
Too many API requests
+
Large images
+
Excessive browser work2. Measure Before Optimizing
The first rule of performance optimization is:
Don’t optimize based on assumptions. Measure first.
Useful tools include:
- React DevTools Profiler
- Chrome Performance panel
- Lighthouse
- Network tab
- Bundle analyzers
- Core Web Vitals
A good optimization workflow is:
Identify problem
↓
Measure
↓
Find bottleneck
↓
Optimize
↓
Measure againFor example, if a dashboard feels slow, first determine whether the problem is:
- React rendering?
- JavaScript execution?
- Network?
- Large images?
- Large DOM?
- Expensive calculation?
Only then choose the optimization technique.
3. Understand Re-renders
Consider:
function App() {
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>
{count}
</button>
<Profile />
</>
);
}When count changes, App renders again.
The important thing to understand is:
A React re-render does not necessarily mean the DOM will be updated.
React can render, compare the result, and determine that some DOM doesn’t need to change.
So: Re-render ≠ DOM update
However, the render work itself can still be expensive.
4. Keep State as Local as Possible
One of the simplest and most effective optimization techniques is state colocation.
Consider:
function App() {
const [search, setSearch] = useState("");
return (
<>
<Navbar />
<Search value={search} onChange={setSearch} />
<HugeDashboard />
<Footer />
</>
);
}Every search update causes App to render again.
If the state is only required by the search section, move it closer to where it’s used:
function SearchSection() {
const [search, setSearch] = useState("");
return (
<>
<Search
value={search}
onChange={setSearch}
/>
<SearchResults search={search} />
</>
);
}
function App() {
return (
<>
<Navbar />
<SearchSection />
<HugeDashboard />
<Footer />
</>
);
}Now the search state update is isolated to a smaller subtree.
Principle
Keep state as close as possible to the components that actually need it.
Don’t move state to the top of the application unless it genuinely needs to be shared.
5. React.memo
React.memo allows React to skip rendering a component when its props haven’t changed.
const UserCard = React.memo(function UserCard({ user }) {
return (
<div>
{user.name}
</div>
);
});Suppose the parent renders again:
Parent renders
↓
UserCard receives same props
↓
React.memo comparison
↓
Skip unnecessary renderThis can be useful when:
- The child is expensive to render
- The child renders frequently
- Props remain stable
- The parent re-renders frequently
6. Referential Equality Matters
Consider:
function App() {
const user = {
name: "Salim"
};
return <UserCard user={user} />;
}Every render creates a new object:
Render 1 → Object A
Render 2 → Object B
Render 3 → Object C
Even though the values are identical:
ObjectA === ObjectB // falseTherefore, React.memo may not be able to skip the render.
This is because JavaScript compares objects and functions by reference.
For example:
{} === {} // false
[] === [] // false7. useMemo
useMemo caches the result of a calculation.
Consider:
const filteredProducts = useMemo(() => {
return products.filter(product =>
product.name
.toLowerCase()
.includes(search.toLowerCase())
);
}, [products, search]);Now the filtering calculation runs when:
products changes OR search changes
If some unrelated state changes:
count changes
↓
Component renders
↓
products unchanged
search unchanged
↓
Reuse memoized result8. When Should You Use useMemo?
useMemo is useful when:
- A calculation is expensive
- A large dataset is being processed
- You need a stable object reference
- You need stable derived data for a memoized child
Example:
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) =>
a.price - b.price
);
}, [products]);But don’t use useMemo for every calculation.
Avoid unnecessary code like:
const fullName = useMemo(
() => `${firstName} ${lastName}`,
[firstName, lastName]
);Rule
Use
useMemowhen the work or referential stability justifies the cost.
9. useCallback
useCallback memoizes a function reference.
Consider:
const ProductList = React.memo(function ProductList({
onDelete
}) {
// ...
});If the parent does this:
function App() {
const handleDelete = (id) => {
deleteProduct(id);
};
return (
<ProductList onDelete={handleDelete} />
);
}a new function is created whenever App renders.
Therefore: oldFunction !== newFunction
ProductList may re-render even though its behavior hasn’t changed.
You can stabilize the reference:
const handleDelete = useCallback((id) => {
deleteProduct(id);
}, []);Now the function reference remains stable as long as its dependencies remain unchanged.
10. Virtualization for Large Lists
Imagine rendering: 100,000 users
with:
users.map(user => (
<UserRow
key={user.id}
user={user}
/>
))This can create a huge DOM tree.
Instead, use virtualization.
The idea is simple:
Render only the items currently visible in the viewport.
For example:
100,000 rows
────────────────────
Visible rows:
501
502
503
...
520
────────────────────Instead of rendering all 100,000 DOM nodes, virtualization renders only a small window of visible items.
Libraries such as react-window and @tanstack/react-virtual can help implement this.
Best use cases
- Large tables
- Chat messages
- Long feeds
- File explorers
- Search results
- Large dropdowns
11. Code Splitting
Large JavaScript bundles can slow down initial page loading.
Instead of:
main.js
├── Home
├── Dashboard
├── Analytics
├── Admin
├── Checkout
└── Editoryou can split the application into smaller chunks:
main.js
home.js
dashboard.js
analytics.js
admin.js
checkout.jsThis is called code splitting.
12. Lazy Loading
React provides lazy() for component-level lazy loading:
const Analytics = lazy(
() => import("./Analytics")
);Then:
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>Now Analytics doesn’t need to be part of the initial critical JavaScript path.
Conceptually:
Initial page
↓
Small initial bundle
↓
User opens Analytics
↓
Analytics chunk downloaded
↓
Skeleton shown
↓
Analytics renderedGood candidates for lazy loading include:
- Admin dashboards
- Analytics
- Rich text editors
- Large charts
- Maps
- Payment interfaces
- Rarely visited routes
13. Don’t Lazy-Load Everything
This isn’t a good idea:
const Button = lazy(() => import("./Button"));For a tiny component, the cost of introducing another asynchronous chunk can outweigh the benefit.
Get Salim Ansari’s stories in your inbox
Join Medium for free to get updates from this writer.
A good rule is:
Lazy-load large or infrequently used features, not every component.
14. Debounce Expensive Search
Consider a search input:
<input onChange={handleSearch} />If the user types:
r
re
rea
reac
reactyou could potentially trigger five API requests.
Instead, debounce the operation:
const handleSearch = debounce((value) => {
fetchResults(value);
}, 300);Now:
User types
r
re
rea
reac
react
↓
Wait 300ms
↓
API requestThis reduces:
- API calls
- Network traffic
- Server work
- Client-side processing
15. Debounce + AbortController
For search experiences, debounce can be combined with AbortController.
User searches "react"
↓
Request A starts
↓
User searches "react hooks"
↓
Abort Request A
↓
Request B starts
↓
Only latest search mattersThis prevents stale requests from continuing unnecessarily.
The important principle is:
Don’t just optimize the request; make the entire interaction cancellation-safe.
That includes:
- Request
- Loading state
- Response handling
- Retries
- UI updates
16. Avoid Unnecessary useEffect
A common performance mistake is using an effect for something that can be calculated directly.
Bad:
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(
`${firstName} ${lastName}`
);
}, [firstName, lastName]);This creates an additional state update:
Render
↓
Effect
↓
setState
↓
Another renderInstead:
const fullName = `${firstName} ${lastName}`;Now there is no unnecessary state update.
Rule
If a value can be derived during rendering, don’t automatically store it in state.
17. Context API and Performance
Context can also cause broad updates.
Consider:
const AppContext = createContext();
function AppProvider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState("dark");
const value = {
user,
setUser,
theme,
setTheme,
};
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
}The provider value is a new object whenever the provider renders.
You can stabilize it when appropriate:
const value = useMemo(() => ({
user,
setUser,
theme,
setTheme,
}), [user, theme]);But there’s a deeper issue.
If one context contains:
User
Theme
Cart
Notifications
Settingsan update to one concern can affect many consumers.
For larger applications, consider splitting contexts:
<UserProvider>
<ThemeProvider>
<CartProvider>
<App />
</CartProvider>
</ThemeProvider>
</UserProvider>18. Optimize Images
React isn’t the only source of performance problems.
Large images can significantly increase page weight.
Instead of blindly loading huge images:
<img src="/huge-product-image.jpg" />use appropriately sized and optimized images.
In Next.js:
<Image
src="/product.jpg"
alt="Product"
width={800}
height={600}
/>Image optimization can improve:
- Loading time
- LCP
- Network usage
- Mobile performance
For image-heavy applications, this can have a larger impact than micro-optimizing React renders.
19. Stable Keys
When rendering lists, use stable and unique keys.
Avoid:
users.map((user, index) => (
<User
key={index}
user={user}
/>
))Prefer:
users.map(user => (
<User
key={user.id}
user={user}
/>
))Keys help React understand the identity of list items during reconciliation.
With stable keys, React can better determine:
. Was an item added?
. Was it removed?
. Did it move?
. Did it change?
Using array indexes can cause problems when items are inserted, deleted, or reordered.
20. Component Composition
Sometimes the best optimization isn’t React.memo.
Suppose:
function App() {
const [count, setCount] = useState(0);
return (
<>
<Counter
count={count}
onIncrement={() =>
setCount(c => c + 1)
}
/>
<ExpensiveComponent />
</>
);
}If ExpensiveComponent doesn’t depend on count, you can structure the component tree so the state lives lower:
function CounterSection() {
const [count, setCount] = useState(0);
return (
<Counter
count={count}
onIncrement={() =>
setCount(c => c + 1)
}
/>
);
}
function App() {
return (
<>
<CounterSection />
<ExpensiveComponent />
</>
);
}Now the state update is localized.
This is another example of:
Good component architecture can be a performance optimization.
21. Server-Side Rendering and React Performance
For frameworks such as Next.js, performance also involves architectural decisions such as:
SSR
SSG
ISR
Streaming
Server Components
Client ComponentsFor example, Server Components can reduce the amount of JavaScript that needs to be sent to the browser for components that don’t require client-side interactivity.
Similarly, static generation can avoid repeating server work for content that doesn’t need to be generated per request.
The correct strategy depends on the application’s requirements.
22. Core Web Vitals
Ultimately, React performance should be measured from the user’s perspective.
Three important Web Vitals are:
LCP — Largest Contentful Paint
Measures how quickly the main content becomes visible.
Slow server/image
↓
Slow LCPINP — Interaction to Next Paint
Measures responsiveness to user interactions.
Click
↓
Long JavaScript task
↓
Delayed UI response
↓
Poor INPCLS — Cumulative Layout Shift
Measures unexpected layout movement.
Image loads
↓
No reserved dimensions
↓
Content moves
↓
Poor CLSTherefore, React performance optimization should ultimately improve real user experience, not just reduce the number shown in the React Profiler.
A Practical Optimization Strategy
Suppose you have a slow dashboard.
Don’t immediately write:
. useMemo();
. useCallback();
. React.memo();
Instead:
Step 1 — Measure
Use the React Profiler.
Dashboard
↓
ProductTable
↓
1000 rowsStep 2 — Identify the bottleneck
Maybe the actual problem is: 1000 DOM rows
Step 3 — Virtualize
1000 rows
↓
20 visible rowsStep 4 — Find expensive calculations
If sorting/filtering is expensive: useMemo(…)
Step 5 — Find unnecessary child renders
If a memoized child receives changing function references: useCallback(…)
Step 6 — Measure again
Confirm that the optimization actually improved performance.
Performance Optimization Checklist
Before shipping a React application, ask:
Rendering
- Are unnecessary components re-rendering?
- Is state located at the correct level?
- Are expensive components isolated?
- Are list keys stable?
Computation
- Are expensive calculations repeated unnecessarily?
- Would
useMemoprovide a meaningful benefit? - Are unnecessary effects causing additional renders?
Functions / References
- Are memoized children receiving unstable callbacks?
- Is
useCallbackactually needed?
Lists
- Are large lists virtualized?
- Are thousands of DOM nodes being rendered unnecessarily?
Network
- Are API requests duplicated?
- Is search debounced?
- Can stale requests be cancelled?
- Are responses cached appropriately?
Bundle
- Is code split?
- Are large features lazy-loaded?
- Are unnecessary dependencies included?
Assets
- Are images appropriately sized?
- Are images optimized?
- Are fonts and other assets loaded efficiently?
UX
- Is LCP good?
- Is INP good?
- Is CLS controlled?
- Are loading and error states handled properly?
Thanks for Reading! 👋
If you found this article helpful, consider following me on Medium for more in-depth articles on JavaScript, React, and modern web development.
I’d love to hear your thoughts! If you have any questions, suggestions, or a different perspective, leave a comment below. Your feedback helps me write better content and creates meaningful discussions within the developer community.
If this article added value to your learning, feel free to share it with other developers who might benefit from it.
Happy Coding! 🚀
— Salim Ansari








