React Performance Optimization: From Slow to Fast
Transform your slow React application into a blazing-fast experience with proven performance optimization techniques and patterns
React Performance Optimization: From Slow to Fast
A slow React application frustrates users and hurts your business. But performance optimization doesn't have to be mysterious. Let's transform your sluggish app into a lightning-fast experience using proven techniques.
Step 1: Measure First, Optimize Second
Never optimize blindly. Use React DevTools Profiler to identify bottlenecks.
Using React DevTools Profiler
// Wrap components to profile
import { Profiler } from 'react';
function onRenderCallback(
id, // component name
phase, // "mount" or "update"
actualDuration, // time spent rendering
baseDuration, // estimated time without memoization
startTime,
commitTime,
interactions // Set of interactions belonging to this update
) {
console.log(`${id}'s ${phase} phase:`);
console.log(`Actual duration: ${actualDuration}ms`);
console.log(`Base duration: ${baseDuration}ms`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}
Chrome DevTools Performance Tab
- Open Chrome DevTools → Performance tab
- Click Record
- Interact with your app
- Stop recording
- Look for long tasks (yellow/red blocks)
Step 2: Prevent Unnecessary Re-renders
The #1 cause of slow React apps is unnecessary re-renders.
Technique 1: React.memo for Component Memoization
// ❌ Bad: Re-renders even when props don't change
function UserCard({ user }) {
console.log('UserCard rendered');
return <div>{user.name}</div>;
}
// ✅ Good: Only re-renders when user changes
const UserCard = React.memo(function UserCard({ user }) {
console.log('UserCard rendered');
return <div>{user.name}</div>;
});
// With custom comparison
const UserCard = React.memo(
function UserCard({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true if you want to skip re-render
return prevProps.user.id === nextProps.user.id;
}
);
Technique 2: useMemo for Expensive Computations
// ❌ Bad: Sorts on every render
function ProductList({ products, filter }) {
const sortedProducts = products
.filter(p => p.category === filter)
.sort((a, b) => b.rating - a.rating);
return sortedProducts.map(p => <ProductCard key={p.id} product={p} />);
}
// ✅ Good: Only sorts when products or filter change
function ProductList({ products, filter }) {
const sortedProducts = useMemo(() => {
return products
.filter(p => p.category === filter)
.sort((a, b) => b.rating - a.rating);
}, [products, filter]);
return sortedProducts.map(p => <ProductCard key={p.id} product={p} />);
}
Technique 3: useCallback for Stable Function References
// ❌ Bad: Creates new function on every render
function TodoList({ todos }) {
const handleToggle = (id) => {
toggleTodo(id);
};
return todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle} // New function reference every render!
/>
));
}
// ✅ Good: Stable function reference
function TodoList({ todos }) {
const handleToggle = useCallback((id) => {
toggleTodo(id);
}, []); // No dependencies, function never changes
return todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
/>
));
}
Step 3: Optimize List Rendering
Large lists are performance killers. Here's how to fix them.
Technique 1: Virtualization with react-window
Only render items visible in the viewport.
import { FixedSizeList } from 'react-window';
// ❌ Bad: Renders 10,000 items
function HugeList({ items }) {
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// ✅ Good: Only renders visible items
function HugeList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
)}
</FixedSizeList>
);
}
Technique 2: Pagination
Break large datasets into pages.
function PaginatedList({ items }) {
const [page, setPage] = useState(1);
const itemsPerPage = 20;
const paginatedItems = useMemo(() => {
const start = (page - 1) * itemsPerPage;
return items.slice(start, start + itemsPerPage);
}, [items, page]);
return (
<div>
{paginatedItems.map(item => (
<ItemCard key={item.id} item={item} />
))}
<Pagination
currentPage={page}
totalPages={Math.ceil(items.length / itemsPerPage)}
onPageChange={setPage}
/>
</div>
);
}
Technique 3: Infinite Scroll
Load more items as user scrolls.
function InfiniteScrollList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const loadMore = async () => {
const newItems = await fetchItems(page);
if (newItems.length === 0) {
setHasMore(false);
return;
}
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
};
return (
<div>
{items.map(item => (
<ItemCard key={item.id} item={item} />
))}
{hasMore && (
<div ref={(node) => {
if (node) {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore();
}
});
observer.observe(node);
return () => observer.disconnect();
}
}}>
Loading more...
</div>
)}
</div>
);
}
Step 4: Code Splitting and Lazy Loading
Don't load everything upfront. Split your bundle and load on demand.
Lazy Load Components
import { lazy, Suspense } from 'react';
// ❌ Bad: Loads entire app upfront
import Dashboard from './Dashboard';
import Settings from './Settings';
import Reports from './Reports';
// ✅ Good: Loads components when needed
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
const Reports = lazy(() => import('./Reports'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
Lazy Load Heavy Libraries
// ❌ Bad: Loads chart library even if not used
import Chart from 'chart.js';
function Dashboard() {
return <div>Dashboard content</div>;
}
// ✅ Good: Only loads when chart is rendered
function Dashboard() {
const [showChart, setShowChart] = useState(false);
const loadChart = async () => {
const Chart = await import('chart.js');
// Use Chart library
};
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && <Suspense fallback="Loading...">
<ChartComponent onLoad={loadChart} />
</Suspense>}
</div>
);
}
Step 5: Optimize State Updates
State updates trigger re-renders. Optimize how and where you update state.
Technique 1: Collocate State
Keep state as close as possible to where it's used.
// ❌ Bad: State at top level affects entire app
function App() {
const [modalOpen, setModalOpen] = useState(false);
const [formData, setFormData] = useState({});
return (
<div>
<Header />
<Sidebar />
<Content />
{modalOpen && <Modal data={formData} />}
</div>
);
}
// ✅ Good: State only where needed
function App() {
return (
<div>
<Header />
<Sidebar />
<Content />
<ModalSection />
</div>
);
}
function ModalSection() {
const [modalOpen, setModalOpen] = useState(false);
const [formData, setFormData] = useState({});
return modalOpen ? <Modal data={formData} /> : null;
}
Technique 2: Split Large State Objects
// ❌ Bad: Entire form re-renders on any field change
function LargeForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
address: '',
// ... 20 more fields
});
return (
<form>
<input
value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}
/>
{/* Every input causes entire form to re-render */}
</form>
);
}
// ✅ Good: Split into separate components
function LargeForm() {
return (
<form>
<NameField />
<EmailField />
<AddressField />
</form>
);
}
function NameField() {
const [name, setName] = useState('');
return <input value={name} onChange={e => setName(e.target.value)} />;
}
Technique 3: Debounce Expensive Operations
import { useMemo, useState, useEffect } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
// Debounce query
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 500);
return () => clearTimeout(timer);
}, [query]);
// Only search when debounced query changes
const results = useMemo(() => {
return expensiveSearch(debouncedQuery);
}, [debouncedQuery]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
<ResultsList results={results} />
</div>
);
}
Step 6: Optimize Images and Assets
Large images slow down your app. Optimize them!
Technique 1: Lazy Load Images
function ImageGallery({ images }) {
return (
<div>
{images.map(img => (
<img
key={img.id}
src={img.url}
loading="lazy" // Browser-native lazy loading
alt={img.alt}
/>
))}
</div>
);
}
Technique 2: Use Next.js Image Component
import Image from 'next/image';
// ❌ Bad: Unoptimized image
<img src="/large-image.jpg" alt="Hero" />
// ✅ Good: Optimized, responsive, lazy-loaded
<Image
src="/large-image.jpg"
alt="Hero"
width={1200}
height={600}
priority // For above-the-fold images
placeholder="blur"
/>
Technique 3: Progressive Image Loading
function ProgressiveImage({ src, placeholder }) {
const [imageSrc, setImageSrc] = useState(placeholder);
const [loading, setLoading] = useState(true);
useEffect(() => {
const img = new Image();
img.src = src;
img.onload = () => {
setImageSrc(src);
setLoading(false);
};
}, [src]);
return (
<div className={loading ? 'loading' : ''}>
<img src={imageSrc} alt="" />
</div>
);
}
Step 7: Web Workers for Heavy Computations
Move CPU-intensive work off the main thread.
// worker.js
self.addEventListener('message', (e) => {
const { data } = e;
// Heavy computation
const result = processLargeDataset(data);
self.postMessage(result);
});
// Component
function DataProcessor() {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker(new URL('./worker.js', import.meta.url));
worker.postMessage(largeDataset);
worker.onmessage = (e) => {
setResult(e.data);
};
return () => worker.terminate();
}, []);
return <div>{result}</div>;
}
Step 8: Use Production Builds
Always use production builds for performance testing.
# Development build (slow, includes warnings)
npm run dev
# Production build (fast, optimized)
npm run build
npm run start
Enable React Production Mode
// Vite config
export default defineConfig({
plugins: [react()],
build: {
minify: 'terser',
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
},
},
},
},
});
Performance Checklist
- Measured performance with React DevTools Profiler
- Wrapped expensive components with React.memo
- Used useMemo for expensive computations
- Used useCallback for stable function references
- Virtualized long lists with react-window
- Implemented code splitting with lazy()
- Lazy loaded heavy libraries
- Collocated state close to usage
- Debounced expensive operations
- Optimized images (lazy loading, proper formats)
- Moved heavy computations to Web Workers
- Using production builds
- Monitored bundle size
- Implemented proper caching strategies
Common Pitfalls
1. Over-optimization
// ❌ Bad: Unnecessary optimization
function SimpleCounter() {
const [count, setCount] = useState(0);
// Useless memo - this is already fast
const displayValue = useMemo(() => count, [count]);
return <div>{displayValue}</div>;
}
2. Incorrect Dependencies
// ❌ Bad: Missing dependencies
const memoizedValue = useMemo(() => {
return expensiveCalc(a, b);
}, [a]); // Missing 'b'!
// ✅ Good: All dependencies included
const memoizedValue = useMemo(() => {
return expensiveCalc(a, b);
}, [a, b]);
3. Premature Optimization
Focus on the biggest bottlenecks first. A 50% improvement on a component that takes 2ms is meaningless, but a 10% improvement on a component that takes 500ms is huge!
Conclusion
React performance optimization is about:
- Measure first - Use React DevTools Profiler
- Prevent re-renders - memo, useMemo, useCallback
- Optimize lists - Virtualization, pagination
- Split code - Lazy loading, dynamic imports
- Optimize state - Collocate, split, debounce
- Optimize assets - Lazy load images, use modern formats
- Production builds - Always test with production builds
Start with the biggest bottlenecks and work your way down. Your users will thank you with better engagement and lower bounce rates!
Jordan Patel
Web Developer & Technology Enthusiast