React performance optimization
Diagnose unnecessary renders with the Profiler before reaching for memo, useMemo, or useCallback.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain react performance optimization in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Explain it simply
Most React performance problems come from rendering more components than necessary, or doing expensive work during a render that runs often. React.memo, useMemo, and useCallback are tools to reduce that work, but they add their own comparison and memory cost, so they should follow measurement, not precede it.
One-line definition: Diagnose unnecessary renders with the Profiler before reaching for memo, useMemo, or useCallback.
02Mental model
Render count is a function of state placement more than any memoization API. State placed high in the tree re-renders everything below it on every change; moving state down to the component that actually needs it often removes more wasted work than wrapping descendants in memo.
03Step by step
- Profile the actual slow interaction with React DevTools Profiler before changing anything.
- Check whether state is placed higher in the tree than it needs to be.
- Move frequently-changing state down or colocate it with what it affects.
- Apply React.memo only where a component's render is measurably expensive and often called with equal props.
- Stabilize identities with useMemo or useCallback only where a downstream memo boundary actually depends on it.
04Working example
// Slow: every keystroke re-renders the whole page, including ExpensiveChartfunction Page() { const [query, setQuery] = useState(''); return ( <> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ExpensiveChart /> </> );}
// Better: move the state down so ExpensiveChart isn't a sibling under itfunction Page() { return ( <> <SearchBox /> <ExpensiveChart /> </> );}In the first version, every keystroke re-renders Page and everything it returns, including ExpensiveChart, because React re-renders a component's entire returned tree by default. Moving query into its own SearchBox component means typing only re-renders SearchBox, with no memo needed.
05Where it is used
- Diagnosing a laggy interaction with the Profiler flame graph
- Deciding whether a list item needs React.memo before virtualizing it
- Avoiding new object or array literals passed as props that defeat memo
- Splitting a large state object so unrelated updates don't cascade
06Common mistakes
- Wrapping every component in React.memo without profiling first, adding comparison cost with no payoff
- Passing a new inline object or arrow function as a prop to a memoized child, silently defeating the memo
- Reaching for useMemo or useCallback as a default habit instead of a measured fix
- Optimizing render count while ignoring that the actual bottleneck was layout or paint, not React
07Interview answer
Lead with measurement and state placement, not a list of memoization APIs — reciting memo, useMemo, and useCallback without profiling first is exactly the shallow answer interviewers are trained to probe past.
A parent passes onSave={() => save(id)} to a React.memo-wrapped child. Does memo actually prevent that child from re-rendering when the parent re-renders?
No — a new arrow function is created on every parent render, so the prop is a new reference every time, which fails memo's shallow equality check and re-renders the child anyway; the function needs to be stabilized with useCallback, or moved, for memo to have any effect.
DDConcept deep dives
Deep dive 1
State placement determines render scope before any API does
A component re-renders, by default, along with everything it returns. State declared high in the tree re-renders every descendant on every change, regardless of whether those descendants actually depend on that state. Moving state down to the smallest component that needs it is usually a bigger win than wrapping many descendants in memo, because it removes the unnecessary render instead of just speeding past it.
- Colocate state with the component that actually reads it.
- A parent re-rendering does not require every descendant's render to be expensive — but it still does the work.
- Structural changes to where state lives often outperform memoization for the same problem.
Deep dive 2
Memoization only works when identity is stable
React.memo compares props with shallow equality — it checks whether each prop is the same reference or primitive value as last time, not whether it's logically equivalent. An inline object, array, or arrow function literal created during the parent's render is a new reference on every call, so a memoized child receiving one of these as a prop re-renders every time regardless of the memo wrapper.
- useMemo and useCallback exist specifically to stabilize identity across renders.
- A memoized component with unstable props gets zero benefit from the memo.
- Measure whether the memo comparison itself is cheap relative to what it's preventing.
// Defeats memo: a new object every render<Row style={{ color: 'red' }} />
// Stable: created once, or memoizedconst rowStyle = useMemo(() => ({ color: 'red' }), []);<Row style={rowStyle} />The first version gives Row a new style object reference on every parent render, failing memo's shallow comparison even though the value never actually changes.
Deep dive 3
Profile before optimizing, always
React DevTools Profiler records which components rendered during an interaction and how long each took, turning a guess about what's slow into a concrete, ordered list of actual costs. Optimizing without profiling risks adding useMemo/useCallback/memo complexity to code that wasn't the bottleneck, while leaving the real cost — often a layout thrash, an expensive computation, or an oversized state object — untouched.
- A flame graph shows exactly which component's render dominated a slow commit.
- Fixing the wrong component adds complexity without improving the metric that mattered.
- Re-profile after each change to confirm it actually helped.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Intermediate · Coding · 1 min · Question 1What determines how many components re-render when state changes?Open model answer
Model answer
State changing in a component triggers a re-render of that component and, by default, everything it returns in its render output. Where state lives in the tree — not the presence of memoization APIs — is the primary driver of how much work a change causes.
// Search text at the top re-renders the whole page on every keystroke.// Move it down so only <SearchBox> and its results re-render:function Page() { return <><Header/><SearchBox/><Sidebar/></>; }Intermediate · Coding · 1 min · Question 2Why can a memoized child still re-render on every parent render?Open model answer
Model answer
React.memo does a shallow comparison of props. An inline object, array, or arrow function created fresh in the parent's render body is a new reference every time, so the shallow comparison always reports a change even though the logical value is the same.
const Child = React.memo(Row);// New `style` and `onClick` identities each render -> memo never hits:<Child style={{ margin: 8 }} onClick={() => pick(id)} />Intermediate · Conceptual · 1 min · Question 3What should come before reaching for useMemo or useCallback?Open model answer
Model answer
Profiling the actual slow interaction with React DevTools Profiler to confirm there's a real, measurable cost, and checking whether state placement or component structure could remove the unnecessary work entirely instead of just memoizing around it.
Open question page →Intermediate · Conceptual · 1 min · Question 4What is the cost of overusing memoization?Open model answer
Model answer
Every memoized value adds a dependency comparison on each render and holds extra memory for the cached result. Applied indiscriminately, the comparison overhead across a large tree can exceed the cost of the renders it was meant to prevent.
Open question page →Intermediate · Conceptual · 1 min · Question 5How does splitting a large state object help performance?Open model answer
Model answer
If one big state object holds many unrelated fields, any single field's update re-renders every consumer of that object. Splitting it into independent pieces of state lets a component subscribe to only the slice it actually needs to react to.
Open question page →Intermediate · Conceptual · 1 min · Question 6Is a re-render always expensive?Open model answer
Model answer
No. A re-render that produces the same output and requires no DOM mutation is usually cheap. The real cost to worry about is expensive computation happening during that render, or an actual DOM/layout change caused by it — not the render call itself.
Open question page →Advanced · Conceptual · 1 min · Question 7What does the React DevTools Profiler's flame graph show?Open model answer
Model answer
It shows how long each component took to render during a recorded commit and which components rendered at all, letting you find the specific component responsible for a slow interaction instead of guessing.
Open question page →Advanced · Conceptual · 1 min · Question 8Why doesn't useMemo guarantee a value is never recalculated?Open model answer
Model answer
React may choose to discard cached memoization results in some circumstances, such as under memory pressure, so useMemo should be treated as a performance hint rather than a semantic guarantee of calling the function only once per dependency change.
Open question page →Advanced · Conceptual · 1 min · Question 9How can virtualization help a long list's render performance?Open model answer
Model answer
It renders only the list items currently visible in the viewport, plus a small buffer, instead of every item in the underlying data, so the render and DOM node count stay roughly constant regardless of list length.
Open question page →Intermediate · Conceptual · 1 min · Question 10Why might moving state into a ref instead of state help a drag interaction?Open model answer
Model answer
A ref update doesn't trigger a re-render at all, so continuous high-frequency updates like pointer position can drive a direct style mutation without paying React's render cost on every event.
Open question page →SCScenario questions
Scenario 1
A dashboard with 200 draggable widgets becomes noticeably laggy while dragging just one of them.
- Profile the drag interaction to see which components render on every drag event.
- Check whether drag position state lives on a shared parent re-rendering all 200 widgets.
- Move the dragged widget's transient position into its own local state or a ref.
- Apply memo to sibling widgets only if profiling still shows unnecessary renders.
Reveal worked answer
I would profile first rather than assume, and I would expect to find drag-position state stored on a parent that re-renders all 200 widget children on every pointer move. I'd move that transient position into the dragged widget's own local state, or drive the visual update through a ref and direct style mutation, so unrelated widgets never re-render during the drag.