React Performance Optimization Techniques
Building a React app that works is step one. Building one that flies is what separates good developers from great ones.
As your React application grows more components, more state, more data performance bottlenecks creep in silently. Pages feel sluggish. Users notice. Conversions drop.
🚀 A 100ms delay in load time can reduce conversions by up to 7%. React gives you powerful tools to prevent this but only if you know where to look.
This guide covers the most effective, battle-tested React performance techniques used by senior engineers at scale from quick wins to deep architectural improvements.
Technique 1 React.memo() : Stop Unnecessary Re-renders
Every time a parent component re-renders, all its children re-render too — even if their props haven't changed. React.memo() fixes this by memoizing the component output.
Without React.memo re-renders on every parent update:
const ProductCard = ({ name, price }) => {
console.log('Re-rendering...'); // fires every time
return <div>{name} — ${price}</div>;
};With React.memo only re-renders when props change:
const ProductCard = React.memo(({ name, price }) => {
return <div>{name} — ${price}</div>;
});✅ Use React.memo when: A component renders often, receives the same props repeatedly, and is moderately expensive to render like list items, cards, or table rows.
❌ Skip React.memo when: The component is cheap to render, or its props change almost every render anyway memoization overhead outweighs the benefit.
Technique 2 useMemo() : Cache Expensive Calculations
If your component runs heavy computations on every render filtering large arrays, sorting data, running algorithms useMemo() caches the result and only recomputes when dependencies change.
Without useMemo recalculates every render:
const FilteredList = ({ products, query }) => {
const results = products
.filter(p => p.name.includes(query))
.sort((a, b) => a.price - b.price);
return <ul>{results.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
};With useMemo cached until products or query changes:
const FilteredList = ({ products, query }) => {
const results = useMemo(() => {
return products
.filter(p => p.name.includes(query))
.sort((a, b) => a.price - b.price);
}, [products, query]);
return <ul>{results.map(p => <li key={p.id}>{p.name}</li>)}</ul>;
};✅ Use useMemo when: Filtering/sorting large arrays, complex mathematical calculations, deriving heavily transformed data from props or state.
Technique 3 useCallback() : Stabilize Function References
In JavaScript, every function is recreated on every render a new reference in memory each time. When you pass functions as props, child components see a "new" prop and re-render even when nothing logically changed. useCallback() solves this.
Without useCallback new function reference on every render:
const Parent = () => {
const handleClick = () => {
console.log('clicked');
};
return <Child onClick={handleClick} />;
// Child re-renders every time Parent renders
};With useCallback stable reference across renders:
const Parent = () => {
const handleClick = useCallback(() => {
console.log('clicked');
}, []); // empty deps = never recreated
return <Child onClick={handleClick} />;
};💡 useCallback is most powerful when combined with React.memo on the child component — together they form a complete shield against unnecessary re-renders.
Technique 4 Code Splitting with React.lazy()
Don't ship your entire app to users on first load. Code splitting breaks your bundle into smaller chunks that load only when needed dramatically improving initial load time.
Without code splitting everything loads upfront:
import Dashboard from './Dashboard';
import Analytics from './Analytics';
import Settings from './Settings';
// All 3 loaded even if user only visits HomeWith React.lazy components load on demand:
import React, { Suspense, lazy } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Analytics = lazy(() => import('./Analytics'));
const Settings = lazy(() => import('./Settings'));
const App = () => (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);✅ Result: Users only download the code for pages they actually visit. A route-based split alone can reduce initial bundle size by 40–60%.
Technique 5 Virtualize Long Lists with React Window
Rendering 1,000+ items into the DOM is one of the most common React performance killers. Virtualization renders only the items currently visible on screen — the rest exist in memory only.
Install the library:
npm install react-windowWithout virtualization all 10,000 rows in the DOM:
const HugeList = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
// 10,000 DOM nodes = browser meltsWith react-window only ~15 visible rows rendered:
import { FixedSizeList } from 'react-window';
const HugeList = ({ items }) => (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={48}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);✅ Real-world impact: Rendering 10,000 rows drops from ~800ms to ~16ms. Your list becomes virtually free to render.
Technique 6 Avoid Anonymous Functions in JSX
This is a micro-optimization that adds up fast in large component trees. Anonymous functions defined inline in JSX create a new function reference on every render even without hooks.
❌ Bad new function reference every render:
<button onClick={() => handleDelete(item.id)}>
Delete
</button>✅ Better stable reference with useCallback:
const handleDelete = useCallback((id) => {
deleteItem(id);
}, []);
<button onClick={() => handleDelete(item.id)}>
Delete
</button>✅ Best for list items, pass the ID through a data attribute:
const handleDelete = useCallback((e) => {
const id = e.currentTarget.dataset.id;
deleteItem(id);
}, []);
<button data-id={item.id} onClick={handleDelete}>
Delete
</button>Technique 7 State Colocation : Keep State Close to Where It's Used
One of the most underrated performance techniques requires zero libraries. Lifting state too high in the component tree causes unnecessary re-renders across huge swaths of your UI.
🔑 Golden rule: State should live as low as possible in the component tree as close as possible to the component that uses it.
❌ Problem global state causing wide re-renders:
const App = () => {
const [searchQuery, setSearchQuery] = useState('');
// Entire App re-renders on every keystroke
return (
<>
<Header />
<Sidebar />
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<Results query={searchQuery} />
</>
);
};✅ Solution colocated state, isolated re-renders:
const SearchSection = () => {
const [searchQuery, setSearchQuery] = useState('');
// Only SearchSection re-renders on keystroke
return (
<>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<Results query={searchQuery} />
</>
);
};Technique 8 Image Optimization
Images are the #1 cause of slow page loads in most React apps. A few simple techniques eliminate most of the damage.
Use lazy loading for below-the-fold images:
<img
src="/product-photo.jpg"
alt="Product"
loading="lazy"
decoding="async"
/>Use next-gen formats and responsive srcSet:
<picture>
<source srcSet="/hero.webp" type="image/webp" />
<source srcSet="/hero.avif" type="image/avif" />
<img
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
loading="eager"
/>
</picture>✅ WebP images are 25–35% smaller than JPEG at the same quality. AVIF is even better — up to 50% smaller.
Performance Techniques Quick Reference
Technique | Problem Solved | Difficulty | Impact |
|---|---|---|---|
React.memo() | Unnecessary child re-renders | Easy | High |
useMemo() | Expensive recalculations | Easy | High |
useCallback() | Unstable function references | Easy | Medium |
Code Splitting | Large initial bundle | Medium | Very High |
List Virtualization | Long list DOM overload | Medium | Very High |
State Colocation | Over-broad re-renders | Medium | High |
Avoid inline functions | Micro re-render overhead | Easy | Low–Medium |
Image Optimization | Slow asset loading | Easy | Very High |
Technique 9 Use the React DevTools Profiler
No optimization is complete without measuring. React DevTools has a built-in Profiler that shows exactly which components are rendering, how often, and how long each render takes.
How to use it:
Install React Developer Tools browser extension
Open DevTools → click the Profiler tab
Click Record → interact with your app → click Stop
Inspect the flame graph — wide orange bars = slow renders
Look for components rendering more than expected
💡 Always profile before optimizing. Premature optimization based on guesses wastes time. The Profiler shows you exactly where your milliseconds are going.
Common Mistakes to Avoid
Wrapping every component in React.memo()— only memoize components with stable props that render frequentlyPutting everything in global state— colocate state as low as possibleOptimizing before profiling— measure first, optimize secondForgetting dependency arrays— wrong deps in useMemo/useCallback cause bugs or defeat the purposeVirtualizing small lists— react-window adds complexity; only use it for 100+ items
The Optimization Priority Order
Follow this sequence for maximum impact with minimum effort:
Profile first — identify the actual bottleneck with React DevTools
Fix state architecture — colocate state, avoid unnecessary lifting
Apply React.memo + useCallback — stop the re-render cascade
Add useMemo — cache expensive computations
Split your code — lazy load routes and heavy components
Virtualize long lists — if rendering 100+ items
Optimize assets — compress and lazy-load images
🎯 Remember: A fast app isn't built with one big optimization. It's built by consistently applying small, well-targeted improvements throughout your codebase.
Conclusion
React performance optimization isn't about tricks — it's about understanding how React works and writing code that works with the rendering model, not against it.
React.memo + useCallback → eliminate unnecessary re-renders
useMemo → cache expensive work
Code splitting + lazy loading → shrink your initial bundle
Virtualization → handle massive lists without melting the browser
State colocation → keep your component tree lean
Profiler → always measure before and after
Don't optimize everything. Optimize the right things — and your React app will feel fast, smooth, and production-ready at any scale.
CodeWithGarry
A passionate writer covering technology, design, and culture.
