React Performance Optimization
Performance Techniques
| Technique | What it does | When to use |
|---|---|---|
React.memo() | Skip re-render if props unchanged | Pure components with expensive renders |
useMemo() | Cache expensive calculation result | Heavy computations in render |
useCallback() | Cache function reference | Callbacks passed to memoized children |
lazy() + Suspense | Code-split and lazy load components | Large components, routes |
useTransition() | Mark updates as non-urgent | Slow renders that shouldn't block UI |
| Virtual lists | Render only visible items | Lists with 1000+ items |
import { memo, useMemo, useCallback, useState } from 'react'
// React.memo — skip re-render if props haven't changed
const ExpensiveItem = memo(function ExpensiveItem({ item, onDelete }) {
console.log('Rendering item:', item.id) // only when item or onDelete changes
return (
<div>
<span>{item.name}</span>
<button onClick={() => onDelete(item.id)}>Delete</button>
</div>
)
})
// Custom comparison function for React.memo
const UserCard = memo(
function UserCard({ user }) {
return <div>{user.name}</div>
},
(prevProps, nextProps) => {
// Return true to SKIP re-render (props are equal)
return prevProps.user.id === nextProps.user.id
&& prevProps.user.name === nextProps.user.name
}
)
function ItemList() {
const [items, setItems] = useState([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
])
const [count, setCount] = useState(0)
// useCallback — stable function reference for memoized children
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id))
}, []) // never changes — no deps
// useMemo — expensive calculation
const stats = useMemo(() => {
console.log('Computing stats...') // only when items changes
return {
total: items.length,
names: items.map(i => i.name).join(', ')
}
}, [items])
return (
<div>
<p>Count: {count} (doesn't re-render items)</p>
<button onClick={() => setCount(c => c + 1)}>+</button>
<p>Stats: {stats.names}</p>
{items.map(item => (
<ExpensiveItem key={item.id} item={item} onDelete={handleDelete} />
))}
</div>
)
}
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
// lazy() — code split at component level
const HeavyDashboard = lazy(() => import('./HeavyDashboard'))
const AdminPanel = lazy(() => import('./AdminPanel'))
const Analytics = lazy(() => import('./Analytics'))
// Loading fallback components
function PageLoader() {
return (
<div className="page-loader">
<div className="spinner"></div>
<p>Loading...</p>
</div>
)
}
function SkeletonCard() {
return (
<div className="skeleton">
<div className="skeleton-title"></div>
<div className="skeleton-text"></div>
<div className="skeleton-text short"></div>
</div>
)
}
// Route-based code splitting (most common pattern)
function App() {
return (
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<HeavyDashboard />} />
<Route path="/admin" element={<AdminPanel />} />
<Route path="/analytics" element={<Analytics />} />
</Routes>
</Suspense>
)
}
// Component-level lazy loading
function Dashboard() {
const [showChart, setShowChart] = useState(false)
const LazyChart = lazy(() => import('./HeavyChart'))
return (
<div>
<button onClick={() => setShowChart(true)}>Load Chart</button>
{showChart && (
<Suspense fallback={<SkeletonCard />}>
<LazyChart />
</Suspense>
)}
</div>
)
}
import { useState, useTransition, useDeferredValue } from 'react'
// useTransition — mark state updates as non-urgent
// Keeps UI responsive during slow renders
function SearchPage() {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [isPending, startTransition] = useTransition()
function handleSearch(e) {
const value = e.target.value
setQuery(value) // urgent — update input immediately
// Non-urgent — can be interrupted by more urgent updates
startTransition(() => {
const filtered = heavyFilter(value) // slow operation
setResults(filtered)
})
}
return (
<div>
<input value={query} onChange={handleSearch} placeholder="Search..." />
{isPending && <p>Updating results...</p>}
<ul style={{ opacity: isPending ? 0.5 : 1 }}>
{results.map(r => <li key={r.id}>{r.name}</li>)}
</ul>
</div>
)
}
// useDeferredValue — defer a value update
function FilteredList({ query }) {
// deferredQuery lags behind query — allows urgent renders first
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
const filtered = useMemo(() =>
items.filter(i => i.name.includes(deferredQuery)),
[deferredQuery]
)
return (
<ul style={{ opacity: isStale ? 0.5 : 1 }}>
{filtered.map(i => <li key={i.id}>{i.name}</li>)}
</ul>
)
}
function heavyFilter(query) {
// Simulate slow computation
return Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }))
.filter(i => i.name.includes(query))
}
Ready to Level Up Your Skills?
Explore 500+ free tutorials across 20+ languages and frameworks.