Chapter 16: Performance Optimization
We’ll go very slowly and clearly, with real-world examples you can copy-paste right now — like I’m sitting next to you in Mumbai showing you live on the screen.
1. Memoization with React.memo (Prevent Unnecessary Re-renders of Components)
React.memo is a higher-order component that memoizes (caches) a component. It prevents the component from re-rendering if its props haven’t changed.
When to use:
- Your component is pure (output depends only on props)
- It renders often but receives the same props most of the time
- It’s expensive to render (big lists, heavy JSX, complex calculations)
Example: Expensive Card Component
Create src/components/ExpensiveCard.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import { memo } from 'react'; interface ExpensiveCardProps { id: number; title: string; description: string; onClick: () => void; } // Without memo → re-renders every time parent re-renders (even if props same!) function ExpensiveCardComponent({ id, title, description, onClick }: ExpensiveCardProps) { console.log(`ExpensiveCard ${id} rendered!`); // ← you'll see this log A LOT without memo // Simulate expensive render let start = performance.now(); while (performance.now() - start < 10) {} // artificial delay ~10ms return ( <div onClick={onClick} style={{ border: '1px solid #ddd', borderRadius: '12px', padding: '20px', margin: '10px', background: '#f8f9ff', cursor: 'pointer', transition: 'transform 0.2s' }} > <h3>{title}</h3> <p>{description}</p> <small>ID: {id}</small> </div> ); } // With memo → only re-renders if props actually change const ExpensiveCard = memo(ExpensiveCardComponent); export default ExpensiveCard; |
Parent Component (without memo → lots of re-renders)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
import { useState } from 'react'; import ExpensiveCard from './ExpensiveCard'; function CardList() { const [count, setCount] = useState(0); const [selectedId, setSelectedId] = useState<number | null>(null); const cards = Array.from({ length: 50 }, (_, i) => ({ id: i + 1, title: `Card ${i + 1}`, description: `This is a very detailed description for card ${i + 1}` })); return ( <div style={{ padding: '40px' }}> <h2>Card List (Count: {count})</h2> <button onClick={() => setCount(c => c + 1)} style={{ padding: '12px 24px', marginBottom: '20px' }} > Increment Counter (causes re-render) </button> <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '20px' }}> {cards.map(card => ( <ExpensiveCard key={card.id} id={card.id} title={card.title} description={card.description} onClick={() => setSelectedId(card.id)} /> ))} </div> <p style={{ marginTop: '30px' }}> Selected card: {selectedId ? `Card ${selectedId}` : 'None'} </p> </div> ); } |
Without memo: Every time you click the counter → all 50 cards re-render → console floods with logs + lag With memo: Only the clicked card re-renders → super fast!
2. useMemo & useCallback in Depth (Memoize Values & Functions)
useMemo – Memoize Expensive Calculations
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import { useMemo, useState } from 'react'; function ExpensiveCalculation() { const [numbers] = useState(() => Array.from({ length: 100000 }, (_, i) => i)); const [filter, setFilter] = useState(50000); // Without useMemo → recalculates on EVERY render (even when filter didn't change) // const filtered = numbers.filter(n => n > filter); // With useMemo → only recalculates when filter or numbers change const filtered = useMemo(() => { console.log('Filtering expensive list...'); return numbers.filter(n => n > filter); }, [numbers, filter]); // dependencies! return ( <div style={{ padding: '40px' }}> <h2>Filtered Numbers (> {filter})</h2> <input type="range" min="0" max="100000" value={filter} onChange={e => setFilter(Number(e.target.value))} style={{ width: '100%' }} /> <p>Count: {filtered.length}</p> </div> ); } |
Key: Only re-calculates when dependencies change.
useCallback – Memoize Functions (Prevent Child Re-renders)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { useState, useCallback } from 'react'; function Child({ onClick }: { onClick: () => void }) { console.log('Child rendered'); return <button onClick={onClick}>Click me</button>; } function Parent() { const [count, setCount] = useState(0); // Without useCallback → new function every render → Child re-renders every time! // const handleClick = () => setCount(c => c + 1); const handleClick = useCallback(() => { setCount(c => c + 1); }, []); // empty deps → stable function return ( <div style={{ padding: '40px' }}> <h2>Count: {count}</h2> <Child onClick={handleClick} /> <button onClick={() => setCount(c => c + 1)}>Increment</button> </div> ); } |
Without useCallback: Child re-renders every time parent re-renders With useCallback: Child only re-renders if props change → much better performance
3. Code Splitting & Lazy Loading (Load Only What You Need)
Lazy loading = load components only when they are needed (great for big apps).
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { lazy, Suspense } from 'react'; // Lazy load heavy components const HeavyDashboard = lazy(() => import('./pages/HeavyDashboard')); const Settings = lazy(() => import('./pages/Settings')); function App() { return ( <Suspense fallback={<div style={{ padding: '100px', textAlign: 'center' }}>Loading...</div>}> <Routes> <Route path="/dashboard" element={<HeavyDashboard />} /> <Route path="/settings" element={<Settings />} /> </Routes> </Suspense> ); } |
Benefits:
- Smaller initial bundle size
- Faster first paint
- Only loads heavy pages when user navigates there
4. React Profiler – Find & Fix Performance Bottlenecks
Wrap slow components with <Profiler> to measure render time.
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import { Profiler } from 'react'; function onRender( id: string, phase: 'mount' | 'update', actualDuration: number, baseDuration: number, startTime: number, commitTime: number ) { console.log(`${id} rendered in ${actualDuration.toFixed(2)}ms`); } function App() { return ( <Profiler id="CardList" onRender={onRender}> <CardList /> </Profiler> ); } |
Even better: Use React Developer Tools Profiler tab (Chrome/Firefox extension)
- Record a session
- See which components are slow
- Find unnecessary re-renders
Summary – Chapter 16 Key Takeaways
- React.memo → prevent component re-renders when props same
- useMemo → memoize expensive values/calculations
- useCallback → memoize functions (especially callbacks to children)
- Code splitting + lazy + Suspense → load only what’s needed
- Profiler → measure & find slow parts
- Rule of thumb: Memoize when you see unnecessary re-renders in console or lag on interactions
Mini Homework
- Take your TodoList or CardList component
- Wrap list items with React.memo
- Memoize any expensive calculations with useMemo
- Memoize event handlers with useCallback
- Bonus: Add <Profiler> and check render times in console
