Most React performance problems come from the same few mistakes. Here's how to diagnose and fix them systematically.
Why Components Re-render
A component re-renders when:
- Its state changes
- Its parent re-renders (even if props didn't change)
- Its context changes
- It calls a hook that triggers a re-render
Understanding this is the foundation. Everything else follows.
Profiling First
Before optimizing anything, use React DevTools Profiler:
Chrome: F12 -> React tab -> Profiler
Click Record, interact with the app, stop recording.
Look for: components with high render time or high render count.
You can also log renders during development:
function ExpensiveComponent({ data }) {
console.count('ExpensiveComponent rendered')
// ...
}
Only optimize what the profiler shows is actually slow.
React.memo
Prevents re-render when props haven't changed:
// Without memo: re-renders every time parent re-renders
function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>
}
// With memo: only re-renders when user prop changes
const UserCard = React.memo(function UserCard({ user }: { user: User }) {
return <div>{user.name}</div>
})
memo uses shallow comparison. If you pass a new object reference every render, it won't help:
// Bad: new object created every render, memo does nothing
<UserCard config={{ showEmail: true }} />
// Good: stable reference
const config = useMemo(() => ({ showEmail: true }), [])
<UserCard config={config} />
useMemo
Cache expensive computations:
// Bad: recalculated every render
function ProductList({ products, filter }) {
const filtered = products.filter(p => p.category === filter) // Runs every render
return filtered.map(p => <ProductCard key={p.id} product={p} />)
}
// Good: only recalculated when dependencies change
function ProductList({ products, filter }) {
const filtered = useMemo(
() => products.filter(p => p.category === filter),
[products, filter]
)
return filtered.map(p => <ProductCard key={p.id} product={p} />)
}
Don't useMemo everything -- it adds overhead. Only memoize:
- Computations that are measurably slow (>1ms)
- Values passed to
memo-wrapped children - Dependencies of
useEffect
useCallback
Stable function references for event handlers passed to memoized children:
function Parent() {
const [count, setCount] = useState(0)
// New function reference every render -> Child always re-renders
const handleClick = () => console.log(count)
// Stable reference -> Child only re-renders when count changes
const handleClick = useCallback(() => console.log(count), [count])
return <Child onClick={handleClick} />
}
const Child = React.memo(({ onClick }) => (
<button onClick={onClick}>Click</button>
))
useCallback is only useful when the function is passed to a memo-wrapped component or used as a useEffect dependency.
State Colocation
Move state down to where it's used:
// Bad: searchQuery state is in root, causes entire tree to re-render on every keystroke
function App() {
const [searchQuery, setSearchQuery] = useState('')
return (
<div>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<HeavyDashboard /> // Re-renders on every keystroke!
<Footer /> // Re-renders on every keystroke!
</div>
)
}
// Good: state is colocated, only SearchBar re-renders
function SearchBar() {
const [query, setQuery] = useState('') // State lives here
return <input value={query} onChange={e => setQuery(e.target.value)} />
}
function App() {
return (
<div>
<SearchBar /> // Only this re-renders
<HeavyDashboard />
<Footer />
</div>
)
}
Virtual Lists for Long Lists
Rendering 10,000 DOM nodes is slow. Only render what's visible:
import { FixedSizeList } from 'react-window'
function VirtualUserList({ users }: { users: User[] }) {
return (
<FixedSizeList
height={600}
itemCount={users.length}
itemSize={72}
width="100%"
>
{({ index, style }) => (
<div style={style}> {/* style contains position -- required */}
<UserCard user={users[index]} />
</div>
)}
</FixedSizeList>
)
}
For variable-height items, use VariableSizeList. For grid layouts, use react-window-infinite-loader for pagination.
The Context Trap
Context re-renders all consumers when its value changes:
// Bad: every consumer re-renders when ANY context value changes
const AppContext = createContext({ user: null, theme: 'dark', cart: [] })
// Good: split contexts by change frequency
const UserContext = createContext(null) // Changes rarely
const ThemeContext = createContext('dark') // Changes rarely
const CartContext = createContext([]) // Changes often
If you must use one context, memoize the value:
function Provider({ children }) {
const [user, setUser] = useState(null)
const [cart, setCart] = useState([])
const value = useMemo(() => ({ user, cart, setCart }), [user, cart])
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}
Most React performance problems are solved by: profiling to find the real bottleneck, colocating state, and virtualizing long lists. memo/useMemo/useCallback are tools for specific scenarios, not defaults to apply everywhere.