Fix React performance problems the right way — understand when components re-render, use React DevTools Profiler, and apply memo/useMemo/useCallback correctly.
React performance optimization is one of the most misunderstood topics in frontend development. Most developers apply memo, useMemo, and useCallback everywhere — which actually makes things slower. This guide shows you how to optimize correctly.
1. Open React DevTools → Profiler tab
2. Click "Record"
3. Interact with the slow part of your app
4. Click "Stop"
5. Look for:
- Components with long render times (yellow/red flame graph)
- Components re-rendering when they shouldn't (check "Why did this render?")
- The same component rendering dozens of times
Don't guess — measure. Premature optimization wastes time and adds complexity.
// A component re-renders when:
// 1. Its own state changes
// 2. Its parent re-renders (even if props didn't change)
// 3. A context it consumes changes
// 4. You call forceUpdate()
function Parent() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>Click {count}</button>
<Child name="Alice" /> {/* re-renders on EVERY parent render */}
</div>
)
}
function Child({ name }: { name: string }) {
console.log("Child rendered") // logs every time Parent renders
return <p>Hello, {name}</p>
}
// Wrap with memo to skip re-render if props are the same
const Child = memo(function Child({ name }: { name: string }) {
console.log("Child rendered") // only logs when `name` changes
return <p>Hello, {name}</p>
})
// ✗ memo won't help if you pass a new object/function each render
function Parent() {
const [count, setCount] = useState(0)
// ✗ New object created on every render — memo is useless
return <Child style={{ color: "red" }} onClick={() => console.log("clicked")} />
}
// Custom comparison function
const ExpensiveList = memo(
({ items }) => <ul>{items.map(i => <li key={i.id}>{i.name}</li>)}</ul>,
(prev, next) => prev.items.length === next.items.length // custom equal check
)
// ✗ Without useMemo — recalculates on every render
function ProductList({ products, filter }) {
const filtered = products
.filter(p => p.category === filter)
.sort((a, b) => b.price - a.price) // expensive if products is large
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
// ✓ With useMemo — only recalculates when products or filter changes
function ProductList({ products, filter }) {
const filtered = useMemo(
() => products
.filter(p => p.category === filter)
.sort((a, b) => b.price - a.price),
[products, filter] // dependency array
)
return <ul>{filtered.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
// ✗ DON'T useMemo for cheap operations — the overhead isn't worth it
const doubled = useMemo(() => count * 2, [count]) // overkill
const doubled = count * 2 // fine — just do this
// ✗ Without useCallback — new function reference on every render
// This breaks React.memo on Child because onClick is always "new"
function Parent() {
const [count, setCount] = useState(0)
const handleClick = () => { // new function every render
console.log("clicked")
}
return <MemoizedChild onClick={handleClick} />
}
// ✓ With useCallback — same function reference between renders
function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => {
console.log("clicked", count)
}, [count]) // only recreates when count changes
return <MemoizedChild onClick={handleClick} />
}
// useCallback = useMemo for functions:
// useCallback(fn, deps) === useMemo(() => fn, deps)
Should I use memo/useMemo/useCallback here?
Is there a measured performance problem?
├── NO → Don't add it. Move on.
└── YES
│
Is this a component re-rendering too often?
├── YES → React.memo (then ensure props are stable with useCallback/useMemo)
└── NO
│
Is this an expensive calculation?
├── YES (>1ms, large data) → useMemo
└── NO → Skip it
Is this a function passed to a memoized child or useEffect dep?
├── YES → useCallback
└── NO → Skip it
// ✗ One big state object — changing anything re-renders everything
const [state, setState] = useState({
user: null,
posts: [],
comments: [],
ui: { darkMode: false, sidebarOpen: true }
})
// ✓ Split state by update frequency
const [user, setUser] = useState(null)
const [posts, setPosts] = useState([])
const [darkMode, setDarkMode] = useState(false)
// ✗ Derived state stored in useState — causes double renders
const [items, setItems] = useState([])
const [count, setCount] = useState(0) // should be derived!
// ✓ Derive it during render (or useMemo if expensive)
const [items, setItems] = useState([])
const count = items.length // derived — no state needed
// ✗ All consumers re-render when any value in context changes
const AppContext = createContext({})
function AppProvider({ children }) {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState("light")
return (
<AppContext.Provider value={{ user, setUser, theme, setTheme }}>
{children}
</AppContext.Provider>
)
}
// ✓ Split contexts by update frequency
const UserContext = createContext(null)
const ThemeContext = createContext("light")
function Providers({ children }) {
return (
<UserContext.Provider value={user}>
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
</UserContext.Provider>
)
}
// Components that use ThemeContext won't re-render when user changes
// Rendering 10,000 DOM nodes is slow no matter what
// Virtualization renders only visible items
import { FixedSizeList } from "react-window"
function VirtualList({ items }) {
return (
<FixedSizeList
height={600} // container height
width="100%"
itemCount={items.length}
itemSize={60} // row height in px
>
{({ index, style }) => (
<div style={style}>
<ListItem item={items[index]} />
</div>
)}
</FixedSizeList>
)
}
// react-window renders only ~10-15 rows at a time regardless of list size