React Performance Optimization
Techniques to make your React apps faster
⚡ What is Performance Optimization?
Performance optimization involves techniques to make React apps faster and more efficient. This includes reducing unnecessary renders, optimizing bundle size, lazy loading components, and using memoization to prevent expensive recalculations.
// Optimize with React.memo
const ExpensiveComponent = React.memo(function({ data }) {
return <div>{data}</div>;
});
Key Optimization Techniques
Memoization
Cache expensive calculations
Prevent Renders
Skip unnecessary re-renders
Code Splitting
Load code on demand
Lazy Loading
Defer component loading
🔹 React.memo for Component Memoization
React.memo prevents re-renders when props haven't changed. Wrap components that render often with the same props to skip unnecessary renders and improve performance significantly.
import React from 'react';
// ❌ Without memo - re-renders every time parent renders
function ExpensiveList({ items }) {
console.log('Rendering list...');
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
// ✅ With memo - only re-renders when items change
const OptimizedList = React.memo(function ExpensiveList({ items }) {
console.log('Rendering list...');
return (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
});
Result:
Component only re-renders when items prop actually changes.
🔹 useMemo for Expensive Calculations
Use useMemo to cache expensive calculations between renders. The calculation only runs when dependencies change, preventing unnecessary work on every render.
import { useMemo, useState } from 'react';
function DataAnalysis({ data }) {
const [filter, setFilter] = useState('');
// ✅ Expensive calculation cached
const processedData = useMemo(() => {
console.log('Processing data...');
return data
.filter(item => item.name.includes(filter))
.map(item => ({
...item,
score: calculateComplexScore(item)
}));
}, [data, filter]); // Only recalculate when these change
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
/>
<DataTable data={processedData} />
</div>
);
}
🔹 useCallback for Function Memoization
useCallback prevents creating new function instances on every render. Essential when passing callbacks to memoized child components to prevent breaking their memoization.
import { useCallback, useState } from 'react';
function TodoList() {
const [todos, setTodos] = useState([]);
// ✅ Function reference stays the same
const handleAddTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text }]);
}, []); // No dependencies - function never changes
const handleDeleteTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []); // No dependencies
return (
<div>
<TodoInput onAdd={handleAddTodo} />
<TodoItems items={todos} onDelete={handleDeleteTodo} />
</div>
);
}
🔹 Lazy Loading Components
Split your app into smaller bundles that load on demand. This dramatically reduces initial load time by only loading what's needed:
import { lazy, Suspense } from 'react';
// ✅ Lazy load heavy components
const Dashboard = lazy(() => import('./Dashboard'));
const Reports = lazy(() => import('./Reports'));
const Settings = lazy(() => import('./Settings'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/reports" element={<Reports />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
🔹 Virtualization for Long Lists
Render only visible items in long lists using virtualization libraries. This keeps the DOM small even with thousands of items:
import { FixedSizeList } from 'react-window';
function VirtualizedList({ items }) {
const Row = ({ index, style }) => (
<div style={style}>
{items[index].name}
</div>
);
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
);
}
// Renders only ~12 items instead of 10,000!
🔹 Debouncing User Input
Delay expensive operations until user stops typing:
import { useState, useEffect } from 'react';
function SearchBox() {
const [query, setQuery] = useState('');
const [debouncedQuery, setDebouncedQuery] = useState('');
// ✅ Debounce search
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedQuery(query);
}, 500); // Wait 500ms after typing stops
return () => clearTimeout(timer);
}, [query]);
// Only search when debounced value changes
useEffect(() => {
if (debouncedQuery) {
performExpensiveSearch(debouncedQuery);
}
}, [debouncedQuery]);
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
);
}
🔹 Key Prop Optimization
Use stable keys for list items to help React identify changes:
// ❌ Bad - index as key
{items.map((item, index) => (
<Item key={index} data={item} />
))}
// ✅ Good - stable unique ID
{items.map(item => (
<Item key={item.id} data={item} />
))}
🔹 Best Practices
✅ Do:
- Profile before optimizing (use React DevTools)
- Use React.memo for expensive components
- Lazy load routes and heavy components
- Virtualize long lists (1000+ items)
- Debounce expensive operations
- Use production builds for testing
❌ Don't:
- Optimize prematurely without measuring
- Wrap every component in React.memo
- Use index as key for dynamic lists
- Forget to memoize callbacks passed to memoized components
- Over-optimize small components