Why Performance Matters
React is fast by default, but as apps grow, performance issues can arise. Learn techniques to optimize rendering, reduce bundle size, and improve user experience.
🎯 React.memo
Prevent unnecessary re-renders of components when props haven't changed.
// Without memo - re-renders on every parent update
function UserCard({ user }) {
console.log('UserCard rendered');
return <div>{user.name}</div>;
}
// With memo - only re-renders when user changes
const UserCard = React.memo(function UserCard({ user }) {
console.log('UserCard rendered');
return <div>{user.name}</div>;
});
function App() {
const [count, setCount] = useState(0);
const user = { name: 'John', id: 1 };
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<UserCard user={user} /> {/* Won't re-render when count changes */}
</div>
);
}
// Custom comparison
const UserCard = React.memo(UserCard, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id;
});
🔧 useMemo Hook
Cache expensive computations between renders.
function ProductList({ products }) {
const [filter, setFilter] = useState('');
// Without useMemo - recalculates on every render
const expensiveFiltered = products.filter(p =>
p.name.toLowerCase().includes(filter.toLowerCase())
).sort((a, b) => b.price - a.price);
// With useMemo - only recalculates when products or filter change
const filteredProducts = useMemo(() => {
console.log('Filtering products...');
return products
.filter(p => p.name.toLowerCase().includes(filter.toLowerCase()))
.sort((a, b) => b.price - a.price);
}, [products, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
{filteredProducts.map(p => (
<div key={p.id}>{p.name} - ${p.price}</div>
))}
</div>
);
}
// Example: Expensive calculation
function DataTable({ data }) {
const [page, setPage] = useState(1);
const statistics = useMemo(() => {
const total = data.reduce((sum, item) => sum + item.value, 0);
const average = total / data.length;
const max = Math.max(...data.map(d => d.value));
return { total, average, max };
}, [data]); // Only recalculate when data changes
return <div>Total: {statistics.total}</div>;
}
🎣 useCallback Hook
Memoize functions to prevent creating new references on each render.
function TodoList() {
const [todos, setTodos] = useState([]);
// Without useCallback - new function on every render
const handleDelete = (id) => {
setTodos(todos.filter(t => t.id !== id));
};
// With useCallback - same function reference
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(t => t.id !== id));
}, []); // Empty deps - function never changes
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete} // Same reference
/>
))}
</div>
);
}
const TodoItem = React.memo(({ todo, onDelete }) => {
console.log('TodoItem rendered');
return (
<div>
{todo.text}
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
);
});
📋 Code Splitting & Lazy Loading
import React, { lazy, Suspense } from 'react';
// Lazy load components
const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));
const Settings = lazy(() => import('./Settings'));
function App() {
const [page, setPage] = useState('dashboard');
return (
<div>
<nav>
<button onClick={() => setPage('dashboard')}>Dashboard</button>
<button onClick={() => setPage('profile')}>Profile</button>
<button onClick={() => setPage('settings')}>Settings</button>
</nav>
<Suspense fallback={<div>Loading...</div>}>
{page === 'dashboard' && <Dashboard />}
{page === 'profile' && <Profile />}
{page === 'settings' && <Settings />}
</Suspense>
</div>
);
}
// Route-based code splitting
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<Spinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
🔄 Virtual Scrolling
Render only visible items in large lists.
// npm install react-window
import { FixedSizeList } from 'react-window';
function LargeList({ 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>
);
}
// Without virtual scrolling - renders 10,000 items
function SlowList({ items }) {
return (
<div>
{items.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
// With virtual scrolling - only renders ~15 visible items
function FastList({ items }) {
return (
<FixedSizeList
height={600}
itemCount={10000}
itemSize={50}
>
{Row}
</FixedSizeList>
);
}
🎨 Debouncing & Throttling
// Debounce - wait for user to stop typing
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const debouncedSearch = useMemo(
() => debounce(async (searchQuery) => {
const data = await fetch(`/api/search?q=${searchQuery}`);
setResults(await data.json());
}, 500),
[]
);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return (
<div>
<input value={query} onChange={handleChange} />
<ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul>
</div>
);
}
function debounce(fn, delay) {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
// Throttle - limit execution rate
function ScrollTracker() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = throttle(() => {
setScrollY(window.scrollY);
}, 100);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>Scroll position: {scrollY}</div>;
}
function throttle(fn, delay) {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn(...args);
}
};
}
📦 Bundle Optimization
// Import only what you need
// ❌ Imports entire library
import _ from 'lodash';
// ✅ Import specific function
import debounce from 'lodash/debounce';
// Tree-shaking friendly imports
// ❌
import { Button, Modal, Dropdown } from 'huge-ui-library';
// ✅
import Button from 'huge-ui-library/Button';
import Modal from 'huge-ui-library/Modal';
// Dynamic imports
// ❌ Import at top level
import HeavyChart from './HeavyChart';
// ✅ Import when needed
function Dashboard() {
const [showChart, setShowChart] = useState(false);
const [Chart, setChart] = useState(null);
useEffect(() => {
if (showChart && !Chart) {
import('./HeavyChart').then(module => {
setChart(() => module.default);
});
}
}, [showChart]);
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
{Chart && <Chart />}
</div>
);
}
🔍 React DevTools Profiler
// Use Profiler to measure performance
import { Profiler } from 'react';
function onRenderCallback(
id, // Component name
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime,
commitTime
) {
console.log(`${id} took ${actualDuration}ms to render`);
}
function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
<UserList />
</Profiler>
);
}
// In browser DevTools:
// 1. Open React DevTools
// 2. Click Profiler tab
// 3. Click record button
// 4. Interact with app
// 5. Stop recording
// 6. Analyze flame graph
📊 Performance Checklist
| Technique | When to Use | Impact |
|---|---|---|
| React.memo | Pure components with same props | High |
| useMemo | Expensive calculations | Medium |
| useCallback | Functions passed to memoized children | Medium |
| Code Splitting | Large routes/components | High |
| Virtual Scrolling | Lists with 100+ items | Very High |
| Debounce/Throttle | Frequent events (scroll, input) | High |
🎯 Key Takeaways
- Don't optimize prematurely: Profile first, then optimize
- React.memo: Prevent unnecessary component re-renders
- useMemo: Cache expensive computation results
- useCallback: Memoize function references
- Code splitting: Load components only when needed
- Virtual scrolling: Essential for large lists
- Debounce: Wait for user to finish before acting
- Bundle size: Import only what you need