Back to Tutorial Overview
React Performance Optimization
Build lightning-fast React applications with proven optimization techniques
Section 1: Component Memoization with React.memo
The Problem: Unnecessary Re-renders
Without memoization, child components re-render whenever parent re-renders, even if props haven't changed:
// ❌ Bad: Child re-renders on every parent update
function ExpensiveChild({ data }) {
console.log('ExpensiveChild rendered');
// Expensive calculations here
return <div>{data.value}</div>;
}
function Parent() {
const [count, setCount] = useState(0);
const data = { value: 'constant' };
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild data={data} />
{/* Child re-renders even though data hasn't changed! */}
</div>
);
}The Solution: React.memo
// ✅ Good: Child only re-renders when props change
const ExpensiveChild = React.memo(function ExpensiveChild({ data }) {
console.log('ExpensiveChild rendered');
return <div>{data.value}</div>;
});
// With custom comparison function
const ExpensiveChild = React.memo(
function ExpensiveChild({ user }) {
return <div>{user.name}</div>;
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return prevProps.user.id === nextProps.user.id;
}
);Section 2: Memoizing Values with useMemo
When to Use useMemo
Use useMemo for expensive calculations that don't need to run on every render:
import { useMemo } from 'react';
function ProductList({ products, filterText }) {
// ❌ Bad: Expensive filter runs on every render
const filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
);
// ✅ Good: Filter only runs when dependencies change
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products.filter(p =>
p.name.toLowerCase().includes(filterText.toLowerCase())
);
}, [products, filterText]);
return (
<div>
{filteredProducts.map(p => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}Real-World Examples
// Example 1: Expensive computation
function DataAnalysis({ data }) {
const analysis = useMemo(() => {
const sum = data.reduce((a, b) => a + b, 0);
const avg = sum / data.length;
const max = Math.max(...data);
const min = Math.min(...data);
return { sum, avg, max, min };
}, [data]);
return <AnalysisDisplay {...analysis} />;
}
// Example 2: Stabilizing object references
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
// ✅ Good: Options object stays stable
const fetchOptions = useMemo(() => ({
headers: { 'Authorization': `Bearer ${token}` },
method: 'GET'
}), [token]);
useEffect(() => {
fetch(`/api/users/${userId}`, fetchOptions)
.then(res => res.json())
.then(setUser);
}, [userId, fetchOptions]);
return <div>{user?.name}</div>;
}Section 3: Memoizing Functions with useCallback
Why useCallback Matters
import { useCallback } from 'react';
const SearchableList = React.memo(function SearchableList({ onSearch }) {
return <input onChange={(e) => onSearch(e.target.value)} />;
});
function Parent() {
const [count, setCount] = useState(0);
// ❌ Bad: New function on every render breaks memoization
const handleSearch = (query) => {
console.log('Searching:', query);
};
// ✅ Good: Stable function reference
const handleSearch = useCallback((query) => {
console.log('Searching:', query);
}, []); // Empty deps = function never changes
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<SearchableList onSearch={handleSearch} />
{/* SearchableList doesn't re-render when count changes */}
</div>
);
}Practical Examples
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
// Memoize handlers passed to child components
const handleAddTodo = useCallback((text) => {
setTodos(prev => [...prev, { id: Date.now(), text, done: false }]);
}, []);
const handleToggleTodo = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, done: !todo.done } : todo
));
}, []);
const handleDeleteTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
return (
<div>
<AddTodoForm onAdd={handleAddTodo} />
<TodoList
todos={todos}
onToggle={handleToggleTodo}
onDelete={handleDeleteTodo}
/>
</div>
);
}Section 4: Code Splitting with React.lazy
Lazy Loading Components
import { lazy, Suspense } from 'react';
// ❌ Bad: Load everything upfront
import HeavyChart from './HeavyChart';
import HeavyDataTable from './HeavyDataTable';
import HeavyEditor from './HeavyEditor';
// ✅ Good: Load on demand
const HeavyChart = lazy(() => import('./HeavyChart'));
const HeavyDataTable = lazy(() => import('./HeavyDataTable'));
const HeavyEditor = lazy(() => import('./HeavyEditor'));
function Dashboard() {
const [activeTab, setActiveTab] = useState('chart');
return (
<div>
<Tabs value={activeTab} onChange={setActiveTab}>
<Tab value="chart">Chart</Tab>
<Tab value="table">Table</Tab>
<Tab value="editor">Editor</Tab>
</Tabs>
<Suspense fallback={<LoadingSpinner />}>
{activeTab === 'chart' && <HeavyChart />}
{activeTab === 'table' && <HeavyDataTable />}
{activeTab === 'editor' && <HeavyEditor />}
</Suspense>
</div>
);
}Route-Based Code Splitting
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Lazy load routes
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={
<div className="loading">
<Spinner />
<p>Loading page...</p>
</div>
}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/profile" element={<Profile />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}Section 5: Optimizing Long Lists
Using react-window for Virtualization
Install react-window: npm install react-window
import { FixedSizeList } from 'react-window';
// ❌ Bad: Rendering 10,000 items
function LargeList({ items }) {
return (
<div>
{items.map(item => (
<div key={item.id} style={{ height: 50 }}>
{item.name}
</div>
))}
</div>
);
}
// ✅ Good: Only render visible items
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>
);
}
// Variable size rows
import { VariableSizeList } from 'react-window';
function DynamicList({ items }) {
const getItemSize = (index) => items[index].height || 50;
return (
<VariableSizeList
height={600}
itemCount={items.length}
itemSize={getItemSize}
width="100%"
>
{Row}
</VariableSizeList>
);
}Section 6: Using React DevTools Profiler
Identifying Performance Issues
1. Open React DevTools
Install the React DevTools extension and open the Profiler tab
2. Record a Session
Click record, interact with your app, then stop recording
3. Analyze Results
Look for components with long render times or frequent re-renders
Programmatic Profiling
import { Profiler } from 'react';
function onRenderCallback(
id, // the "id" prop of the Profiler tree
phase, // "mount" or "update"
actualDuration, // time spent rendering
baseDuration, // estimated time without memoization
startTime, // when React began rendering
commitTime, // when React committed the update
interactions // Set of interactions
) {
console.log(`${id} took ${actualDuration}ms to render`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
);
}Keep Optimizing!
Performance optimization is an ongoing process. Always measure before optimizing!
Back to Tutorial Overview