React Performance Optimization: Practical Techniques That Actually Work
React is fast by default, but as your application grows, performance can become an issue. The good news? Most performance problems have straightforward solutions once you know what to look for.
Measure First, Optimize Second
Never optimize without measuring. Use React DevTools Profiler to identify actual bottlenecks:
import { Profiler } from 'react';
function onRenderCallback(id, phase, actualDuration) {
console.log(`${id} (${phase}) took ${actualDuration}ms`);
}
<Profiler id="MyComponent" onRender={onRenderCallback}>
<MyComponent />
</Profiler>
Memoization Strategies
React.memo()
Prevent unnecessary re-renders of component:
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* Expensive rendering */}</div>;
});
// Only re-renders if data changes
useMemo()
Memoize expensive calculations:
function DataTable({ data, filter }) {
const filteredData = useMemo(() => {
return data.filter(item => item.category === filter);
}, [data, filter]); // Only recalculates when data or filter changes
return <Table data={filteredData} />;
}
useCallback()
Memoize function references:
function ParentComponent() {
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []); // Function reference stays the same
return items.map(item => (
<MemoizedChild key={item.id} onClick={handleClick} />
));
}
Code Splitting
Don't load code users don't need:
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
Virtualize Long Lists
Don't render 10,000 items—render what's visible:
import { FixedSizeList } from 'react-window';
function VirtualList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
}
Optimize Context Usage
Avoid re-rendering every consumer:
// ❌ Bad: Everything re-renders when any value changes
const AppContext = createContext();
function Provider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
);
}
// ✅ Good: Split contexts by concern
const UserContext = createContext();
const ThemeContext = createContext();
function Provider({ children }) {
const [user, setUser] = useState(null);
const [theme, setTheme] = useState('light');
return (
<UserContext.Provider value={{ user, setUser }}>
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
);
}
Debounce Expensive Operations
import { useDeferredValue } from 'react';
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(
() => expensiveSearch(deferredQuery),
[deferredQuery]
);
return <ResultsList results={results} />;
}
Use Keys Correctly
Keys help React identify which items changed:
// ❌ Bad: Index as key causes issues with reordering
{items.map((item, index) => (
<Item key={index} data={item} />
))}
// ✅ Good: Stable unique ID
{items.map(item => (
<Item key={item.id} data={item} />
))}
Avoid Inline Object/Array Creation
// ❌ Bad: Creates new object every render
<Component style={{ margin: 10 }} />
<Component items={[1, 2, 3]} />
// ✅ Good: Stable reference
const style = { margin: 10 };
const items = [1, 2, 3];
<Component style={style} />
<Component items={items} />
Optimize Images
// Use next/image or similar for automatic optimization
import Image from 'next/image';
<Image
src="/photo.jpg"
width={800}
height={600}
loading="lazy"
placeholder="blur"
/>
State Colocation
Keep state as close as possible to where it's used:
// ❌ Bad: State at top level causes entire tree to re-render
function App() {
const [hoveredId, setHoveredId] = useState(null);
return <UserList users={users} hoveredId={hoveredId} setHoveredId={setHoveredId} />;
}
// ✅ Good: State in the component that needs it
function UserCard({ user }) {
const [isHovered, setIsHovered] = useState(false);
return <div onMouseEnter={() => setIsHovered(true)}>{/* ... */}</div>;
}
Use Production Builds
Development builds are much slower:
# Development (slow, includes debugging)
npm start
# Production (fast, optimized)
npm run build
npm run preview
Lazy Load Images
<img
src="image.jpg"
loading="lazy"
decoding="async"
alt="Description"
/>
Web Workers for Heavy Computation
Move heavy computation off the main thread:
import { useEffect, useState } from 'react';
function DataProcessor({ data }) {
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker('./processor.worker.js');
worker.postMessage(data);
worker.onmessage = (e) => setResult(e.data);
return () => worker.terminate();
}, [data]);
return <Display result={result} />;
}
The 80/20 Rule
Focus on the biggest wins:
- Code splitting - Usually the biggest impact
- Virtualization - For long lists
- Image optimization - Often overlooked
- Memoization - Only where it matters
Don't micro-optimize everything. Profile, find the bottleneck, fix it, measure again.
Performance Budget
Set and enforce performance budgets:
- Initial load: < 3 seconds
- Time to Interactive: < 5 seconds
- Lighthouse score: > 90
Use tools like Lighthouse and WebPageTest to measure.
Keep Learning
React is constantly evolving. Stay updated with:
Need help optimizing your React app? Get in touch.