React Performance Optimization: A Complete Guide
Master React performance optimization techniques including memo, useMemo, useCallback, code splitting, and more to build blazing-fast applications.
React Performance Optimization: A Complete Guide
Performance is crucial for modern web applications. Users expect instant interactions and smooth experiences. In this comprehensive guide, I'll share proven techniques to optimize your React applications.
Understanding React Rendering
Before optimizing, understand how React renders components:
// React re-renders when:
// 1. State changes
// 2. Props change
// 3. Parent component re-renders
// 4. Context value changes
function ParentComponent() {
const [count, setCount] = useState(0)
// Child re-renders even if its props don't change
return (
<div>
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<ExpensiveChild />
</div>
)
}
1. React.memo for Component Memoization
Prevent unnecessary re-renders by memoizing components:
import { memo } from 'react'
interface UserCardProps {
user: {
id: string
name: string
email: string
}
}
// Without memo - re-renders on every parent update
function UserCard({ user }: UserCardProps) {
console.log('Rendering UserCard')
return (
<div>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
)
}
// With memo - only re-renders when user changes
export default memo(UserCard)
// Custom comparison function
export default memo(UserCard, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id
})
2. useMemo for Expensive Calculations
Memoize expensive computations:
import { useMemo } from 'react'
function ProductList({ products }: { products: Product[] }) {
// ❌ Runs on every render
const sortedProducts = products.sort((a, b) => b.price - a.price)
// ✅ Only recalculates when products change
const sortedProducts = useMemo(() => {
console.log('Sorting products...')
return products.sort((a, b) => b.price - a.price)
}, [products])
return (
<div>
{sortedProducts.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
3. useCallback for Function References
Stabilize function references to prevent unnecessary re-renders:
import { useState, useCallback, memo } from 'react'
function TodoList() {
const [todos, setTodos] = useState<Todo[]>([])
// ❌ New function on every render
const handleDelete = (id: string) => {
setTodos(todos.filter(todo => todo.id !== id))
}
// ✅ Stable function reference
const handleDelete = useCallback((id: string) => {
setTodos(prev => prev.filter(todo => todo.id !== id))
}, [])
return (
<div>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onDelete={handleDelete}
/>
))}
</div>
)
}
// TodoItem won't re-render if onDelete reference is stable
const TodoItem = memo(({ todo, onDelete }: TodoItemProps) => {
return (
<div>
<span>{todo.title}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
)
})
4. Code Splitting with React.lazy
Split your bundle for faster initial loads:
import { lazy, Suspense } from 'react'
import { LoadingSpinner } from './LoadingSpinner'
// Lazy load heavy components
const Dashboard = lazy(() => import('./Dashboard'))
const Analytics = lazy(() => import('./Analytics'))
const Settings = lazy(() => import('./Settings'))
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
)
}
5. Virtualization for Long Lists
Use virtualization for lists with many items:
import { FixedSizeList } from 'react-window'
function VirtualizedList({ items }: { items: string[] }) {
const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
<div style={style}>
Item {items[index]}
</div>
)
return (
<FixedSizeList
height={600}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</FixedSizeList>
)
}
6. Debouncing and Throttling
Control the rate of expensive operations:
import { useState, useMemo } from 'react'
function SearchInput() {
const [query, setQuery] = useState('')
// Debounce search - wait for user to stop typing
const debouncedSearch = useMemo(() => {
let timeout: NodeJS.Timeout
return (value: string) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
performSearch(value)
}, 300)
}
}, [])
return (
<input
type="text"
value={query}
onChange={(e) => {
setQuery(e.target.value)
debouncedSearch(e.target.value)
}}
/>
)
}
// Custom hook for debouncing
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(timeout)
}, [value, delay])
return debouncedValue
}
7. Optimize Context Usage
Prevent unnecessary context re-renders:
import { createContext, useContext, useMemo } from 'react'
// ❌ Single context with all state
const AppContext = createContext({
user: null,
theme: 'light',
settings: {}
})
// ✅ Split contexts by update frequency
const UserContext = createContext(null)
const ThemeContext = createContext('light')
const SettingsContext = createContext({})
// Memoize context values
function UserProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState(null)
// Prevent re-renders when user object reference changes
const value = useMemo(() => ({ user, setUser }), [user])
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
)
}
8. Use Keys Correctly
Proper key usage helps React identify changes:
// ❌ Using index as key (anti-pattern for dynamic lists)
{items.map((item, index) => (
<Item key={index} data={item} />
))}
// ✅ Using stable unique identifier
{items.map((item) => (
<Item key={item.id} data={item} />
))}
// ✅ For static lists, index is fine
{['Home', 'About', 'Contact'].map((link, index) => (
<NavLink key={index}>{link}</NavLink>
))}
9. Optimize Images
Images are often the biggest performance bottleneck:
import Image from 'next/image'
function ProductGallery() {
return (
<div>
{/* ✅ Next.js Image with automatic optimization */}
<Image
src="/product.jpg"
alt="Product"
width={800}
height={600}
placeholder="blur"
loading="lazy"
quality={75}
/>
{/* ✅ Native lazy loading */}
<img
src="/banner.jpg"
alt="Banner"
loading="lazy"
decoding="async"
/>
</div>
)
}
10. Use Web Workers for Heavy Computations
Offload heavy computations to web workers:
// worker.ts
self.addEventListener('message', (e) => {
const result = heavyComputation(e.data)
self.postMessage(result)
})
// Component
function DataProcessor() {
const [result, setResult] = useState(null)
useEffect(() => {
const worker = new Worker(new URL('./worker.ts', import.meta.url))
worker.postMessage(largeDataset)
worker.addEventListener('message', (e) => {
setResult(e.data)
})
return () => worker.terminate()
}, [])
return <div>{result}</div>
}
11. Profile Before Optimizing
Use React DevTools Profiler:
import { Profiler } from 'react'
function App() {
const onRenderCallback = (
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) => {
console.log(`${id} took ${actualDuration}ms to render`)
}
return (
<Profiler id="App" onRender={onRenderCallback}>
<Dashboard />
</Profiler>
)
}
12. Avoid Inline Functions and Objects
Inline definitions create new references on every render:
// ❌ Inline object - new reference each render
<UserProfile user={{ name: 'John', age: 30 }} />
// ✅ Stable reference
const user = { name: 'John', age: 30 }
<UserProfile user={user} />
// ❌ Inline function - new reference each render
<Button onClick={() => handleClick(id)} />
// ✅ Use useCallback
const handleButtonClick = useCallback(() => handleClick(id), [id])
<Button onClick={handleButtonClick} />
Performance Checklist
- ✅ Use React.memo for expensive components
- ✅ Memoize expensive calculations with useMemo
- ✅ Stabilize function references with useCallback
- ✅ Implement code splitting with lazy loading
- ✅ Virtualize long lists
- ✅ Debounce/throttle expensive operations
- ✅ Optimize context usage
- ✅ Use correct keys in lists
- ✅ Lazy load images
- ✅ Profile before optimizing
- ✅ Avoid inline functions and objects in JSX
Conclusion
Performance optimization is about finding the right balance. Don't optimize prematurely - measure first, then optimize the bottlenecks. Use React DevTools Profiler to identify slow components and apply these techniques strategically.
Remember: Premature optimization is the root of all evil. Start with clean, readable code, then optimize based on real performance metrics.
Tools and Resources
Happy optimizing! ⚡
Related Posts
Building pyfs-watcher: A Rust-Powered Filesystem Toolkit for Python
How I built a high-performance filesystem toolkit for Python using Rust and PyO3 — parallel directory walking, BLAKE3 hashing, file deduplication, and real-time watching, all from pip install.
How I Built This Developer Portfolio
A technical walkthrough of building my portfolio site with Next.js 15, TypeScript, Tailwind CSS, and a dual-source blog system powered by MDX files and SQLite.
TypeScript Best Practices for Production Applications
Learn essential TypeScript best practices and patterns to write type-safe, maintainable code for production-ready applications.
Enjoyed this post?
Subscribe to get notified when I publish new content about web development and technology.