In the world of React development, performance optimization isn't just a "nice-to-have" – it's a critical skill that separates good apps from great ones. After optimizing dozens of production React applications, I've distilled these 7 battle-tested performance patterns you can immediately implement in your projects.
1. Memoization Magic: useMemo
& useCallback
The Problem: Unnecessary re-renders from unchanged props/state
The Solution: Cache expensive computations and function references
const ExpensiveComponent = ({ items }) => {
// Only recalculates when `items` changes
const sortedList = useMemo(() =>
items.sort((a, b) => a.price - b.price),
[items]);
// Stable function reference
const handleClick = useCallback(() => {
console.log('Item clicked:', sortedList[0]);
}, [sortedList]);
return <ChildComponent onClick={handleClick} />;
};
When to Use:
- Heavy computations (sorting, filtering, data transformations)
- Passing callbacks to optimized child components (
React.memo
) - Context providers passing stable values
Pro Tip: Combine with React.memo
for child components to prevent unnecessary tree updates.
2. Lazy Loading & Code Splitting
The Problem: Bloated initial bundle sizes slowing down FCP (First Contentful Paint)
The Solution: Dynamic imports + Suspense for gradual loading
const HeavyChartLibrary = React.lazy(() => import('./ChartComponent'));
const Dashboard = () => (
<React.Suspense fallback={<Spinner />}>
{showCharts && <HeavyChartLibrary />}
</React.Suspense>
);
Advanced Pattern: Route-based splitting with React Router:
const routes = [
{
path: '/analytics',
element: (
<React.Suspense fallback={null}>
<AnalyticsPage />
</React.Suspense>
),
},
];
When to Use:
- Non-critical above-the-fold components
- Routes
- Heavy third-party libraries
3. Virtualized Lists for Massive Datasets
The Problem: DOM node overload from rendering thousands of items
The Solution: Render only visible items with react-window
import { FixedSizeList as List } from 'react-window';
const BigList = ({ items }) => (
<List
height={600}
itemCount={items.length}
itemSize={35}
width="100%"
>
{({ index, style }) => (
<div style={style}>{items[index].name}</div>
)}
</List>
);
Bonus: For dynamic heights, use VariableSizeList
. Pair with react-virtualized-auto-sizer
for responsive containers.
4. Smart State Batching
The Problem: Multiple state updates triggering sequential re-renders
The Solution: Leverage React 18+ automatic batching
Classic Approach (Pre-React 18):
// Triggers two re-renders
setCount(1);
setText('Updated');
Optimized with React 18 Automatic Batching:
// Single re-render
ReactDOM.flushSync(() => {
setCount(1);
setText('Updated');
});
Advanced Pattern: Use useReducer
for complex state updates:
const [state, dispatch] = useReducer(reducer, initialState);
// Single update
dispatch({ type: 'UPDATE_BOTH', count: 1, text: 'Updated' });
5. Debounced API Calls in Hooks
The Problem: API spamming from rapid user input (search bars, filters)
The Solution: useDebounce
custom hook
import { useEffect, useState } from 'react';
const useDebouncedValue = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
};
// Usage in component
const SearchComponent = () => {
const [query, setQuery] = useState('');
const debouncedQuery = useDebouncedValue(query, 300);
useEffect(() => {
fetchResults(debouncedQuery); // API call
}, [debouncedQuery]);
};
Pro Tip: Combine with AbortController
to cancel pending requests.
6. Optimized Context API Usage
The Problem: Context consumers re-rendering when unrelated values change
The Solution: Split contexts + memoization
Bad Practice:
// All consumers re-render when any value changes
const AppContext = createContext({
user: null,
theme: 'light'
});
Optimized Approach:
// Split into logical contexts
const UserContext = createContext(null);
const ThemeContext = createContext('light');
// Memoized provider value
const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const value = useMemo(() => ({ theme, setTheme }), [theme]);
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
};
Advanced Pattern: Use context selectors with use-context-selector
library.
7. Optimistic UI Updates
The Problem: Waiting for API responses creates sluggish interfaces
The Solution: Instant visual feedback + rollback on error
const TodoList = () => {
const [todos, setTodos] = useState([]);
const addTodo = async (text) => {
const tempId = Date.now();
// Optimistic update
setTodos(prev => [...prev, { id: tempId, text, status: 'pending' }]);
try {
const savedTodo = await api.saveTodo(text);
setTodos(prev =>
prev.map(t => t.id === tempId ? savedTodo : t)
);
} catch (error) {
// Rollback on error
setTodos(prev => prev.filter(t => t.id !== tempId));
}
};
};
Key Benefits:
- Instant perceived performance
- Reduced "loading spinner fatigue"
- Smooth user experience
Putting It All Together: Performance Checklist
- Audit re-renders with React DevTools Profiler
- Measure bundle size with
source-map-explorer
- Test with Chrome's Performance tab (CPU throttling)
- Use
React.memo
/useMemo
/useCallback
judiciously - Implement incremental loading strategies
- Optimize images/media with lazy loading
- Server-side render critical content
Remember: Premature optimization is the root of all evil – profile first, optimize second!
FAQ: React Performance Patterns
Q: How do I choose between useMemo
and useCallback
?
A: useMemo
for values, useCallback
for functions
Q: Does React 18 automatic batching work everywhere?
A: Yes, except in async operations (wrap in flushSync
if needed)
Q: When should I avoid virtualization?
A: For lists under 100 items – the overhead outweighs benefits
By implementing these patterns, you'll create React apps that feel snappier, consume less memory, and keep users engaged. The best part? All these techniques work with any React stack – whether you're using Next.js, Gatsby, or vanilla React.
Top comments (0)