A comprehensive checklist of performance optimizations every React developer should know.
1. Component Optimization
Use React.memo for Pure Components
Prevent unnecessary re-renders for components that don't need to update:
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{data.content}</div>;
});
// With custom comparison
const MyComponent = React.memo(({ user }) => {
return <div>{user.name}</div>;
}, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
});
When to use: Components that receive the same props frequently, or expensive render operations.
useMemo for Expensive Calculations
Cache computed values to avoid recalculation on every render:
const ExpensiveList = ({ items, filter }) => {
const filteredItems = useMemo(() => {
return items.filter(item => item.category === filter)
.sort((a, b) => b.price - a.price);
}, [items, filter]);
return <div>{filteredItems.map(item => <Item key={item.id} {...item} />)}</div>;
};
Remember: Only use for genuinely expensive operations. Don't over-optimize.
useCallback for Stable Function References
Prevent child components from re-rendering when passing callbacks:
const ParentComponent = () => {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Button clicked');
// Some logic here
}, []); // Dependencies array
return <ChildComponent onClick={handleClick} />;
};
When to use: Functions passed to memoized child components, or functions used as dependencies in useEffect.
2. List Rendering Optimization
Always Use Proper Keys
// ❌ Bad - Using index
{items.map((item, index) => <Item key={index} {...item} />)}
// ✅ Good - Using unique ID
{items.map(item => <Item key={item.id} {...item} />)}
Why? Indexes can cause incorrect re-renders when items are added, removed, or reordered.
Virtualize Long Lists
For lists with 100+ items, use virtualization:
import { FixedSizeList } from 'react-window';
const VirtualList = ({ items }) => (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</FixedSizeList>
);
Libraries: react-window, react-virtualized
3. Code Splitting & Lazy Loading
Lazy Load Route Components
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const App = () => (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
);
Lazy Load Heavy Components
const HeavyChart = lazy(() => import('./HeavyChart'));
const Dashboard = () => {
const [showChart, setShowChart] = useState(false);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
)}
</div>
);
};
4. State Management Optimization
Lift State Down, Not Always Up
// ❌ Bad - Unnecessary state at top level
const App = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return <div><Navbar /><Content /><Modal open={isModalOpen} /></div>;
};
// ✅ Good - State where it's needed
const ModalContainer = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
return <Modal open={isModalOpen} onClose={() => setIsModalOpen(false)} />;
};
Split Context Providers
// ❌ Bad - One massive context
const AppContext = createContext({ user, theme, notifications, settings });
// ✅ Good - Separate contexts
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();
Why? Changes to one part won't trigger re-renders for components using other parts.
Use Context Selectors
// For complex contexts, use a selector pattern
const useUser = () => {
const context = useContext(AppContext);
return context.user;
};
const useTheme = () => {
const context = useContext(AppContext);
return context.theme;
};
5. Image Optimization
Lazy Load Images
<img
src={image.url}
loading="lazy"
alt={image.alt}
/>
Use Modern Formats
<picture>
<source srcSet={image.avif} type="image/avif" />
<source srcSet={image.webp} type="image/webp" />
<img src={image.jpg} alt={image.alt} />
</picture>
Proper Sizing
// Always specify width and height to prevent layout shift
<img
src={image.url}
width={800}
height={600}
alt={image.alt}
/>
6. Effect Optimization
Clean Up Effects
useEffect(() => {
const subscription = api.subscribe(data => {
setData(data);
});
// Always clean up!
return () => {
subscription.unsubscribe();
};
}, []);
Debounce Expensive Operations
import { debounce } from 'lodash';
const SearchComponent = () => {
const [query, setQuery] = useState('');
const debouncedSearch = useMemo(
() => debounce((value) => {
// Expensive API call
fetchResults(value);
}, 300),
[]
);
const handleChange = (e) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
return <input value={query} onChange={handleChange} />;
};
7. Bundle Size Optimization
Import Only What You Need
// ❌ Bad
import _ from 'lodash';
import { Button } from '@mui/material';
// ✅ Good
import debounce from 'lodash/debounce';
import Button from '@mui/material/Button';
Analyze Your Bundle
# Create-React-App
npm run build -- --stats
npx webpack-bundle-analyzer build/bundle-stats.json
# Vite
npm run build
npx vite-bundle-visualizer
Use Dynamic Imports for Large Libraries
// Only load when needed
const handleExport = async () => {
const XLSX = await import('xlsx');
XLSX.writeFile(workbook, 'data.xlsx');
};
8. Render Optimization
Avoid Inline Functions in Props
// ❌ Bad - Creates new function on every render
<Child onClick={() => handleClick(id)} />
// ✅ Good - Stable function reference
const handleChildClick = useCallback(() => handleClick(id), [id]);
<Child onClick={handleChildClick} />
Avoid Creating Objects in Render
// ❌ Bad - New object every render
<Child style={{ margin: 10, padding: 20 }} />
// ✅ Good - Define outside or use useMemo
const childStyle = { margin: 10, padding: 20 };
<Child style={childStyle} />
9. Third-Party Script Optimization
Load Scripts Asynchronously
useEffect(() => {
const script = document.createElement('script');
script.src = 'https://example.com/widget.js';
script.async = true;
document.body.appendChild(script);
return () => {
document.body.removeChild(script);
};
}, []);
Use Web Workers for Heavy Processing
// worker.js
self.onmessage = (e) => {
const result = heavyCalculation(e.data);
self.postMessage(result);
};
// Component
const MyComponent = () => {
useEffect(() => {
const worker = new Worker('/worker.js');
worker.postMessage(data);
worker.onmessage = (e) => {
setResult(e.data);
};
return () => worker.terminate();
}, []);
};
10. Monitoring & Measuring
Use React DevTools Profiler
import { Profiler } from 'react';
<Profiler id="MyComponent" onRender={(id, phase, actualDuration) => {
console.log(`${id} took ${actualDuration}ms`);
}}>
<MyComponent />
</Profiler>
Lighthouse Performance Audits
Regular checks:
- Run Lighthouse in Chrome DevTools
- Check Core Web Vitals (LCP, FID, CLS)
- Monitor bundle size over time
- Test on throttled connections
Performance Monitoring Tools
- Chrome DevTools Performance - Record runtime performance
- React DevTools Profiler - Component render times
- Webpack Bundle Analyzer - Bundle size analysis
- Web Vitals - Real user metrics
Quick Checklist
Before deploying, verify:
- All images have
loading="lazy"
attribute - List items use stable, unique keys
- Heavy components are code-split
- Expensive calculations wrapped in
useMemo
- Callbacks to memoized children use
useCallback
- Large contexts are split into smaller ones
- Third-party scripts load asynchronously
- Bundle size analyzed and optimized
- Lighthouse score > 90
- No unnecessary re-renders (check with React DevTools)
Common Anti-Patterns to Avoid
- Premature optimization - Measure first, optimize second
- Over-memoization - Not everything needs
useMemo
/useCallback
- Large context providers - Split into logical chunks
- Inline styles - Extract to constants or CSS
- Massive component trees - Code split and lazy load
- Fetching in useEffect - Use proper data fetching libraries (React Query, SWR)
- Ignoring bundle size - Regularly audit with analyzer tools
Advanced Tips
Use Transition API (React 18+)
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
const handleTabChange = (tab) => {
startTransition(() => {
setTab(tab); // Non-urgent update
});
};
Optimize Re-renders with useId
// Stable IDs across server/client
const id = useId();
<input id={id} aria-describedby={`${id}-description`} />
Consider Server Components (Next.js 13+)
Move non-interactive parts to server components for zero JS shipped to client.
Remember
Test on real devices, real networks, and real scenarios. Your users will thank you.
Last updated: October 2025