the four re-render reasons (that's all there are)
Every wasted render traces to exactly one of four causes. The React DevTools Profiler records each commit (a render+paint cycle) and shows a flamegraph (component tree with render times), a ranked chart (components sorted by cost), and — with "Record why each component rendered" on — the commit reason for every component. Click a bar to read its reason.
| reason | trigger | who re-renders |
|---|---|---|
| parent re-rendered | any ancestor rendered | all non-memoized descendants (the default cascade) |
| state changed | useState / useReducer setter |
that component + its non-memoized descendants |
| context changed | a Provider value updated |
every useContext consumer — bypasses memo |
| props changed | parent passed a new reference / value | the child — a React.memo child only if the prop fails shallow Object.is |
Fixes map 1:1 to causes: React.memo blocks parent-cascade & prop-wiggle;
useMemo/useCallback stabilize props so memo can skip; splitting
context limits context blast radius; moving state down limits the cascade.
1 · the profiler you write (edit me)
2 · Babel compiles JSX → element tree
<Profiler id="tree" onRender={'{flushCommit}'}> wraps the subtree. React calls
flushCommit(id, phase, actualDuration, ...) at the end of every commit — the
moment to drain BUFFER into a visible commit. The phase arg is
"mount", "update", or "nested-update".
// (hit "compile & render" to see Babel's output)
3 · live React (the four reasons, proven)
Click Update Parent State → Parent + all 3 children render (state changed → cascade). Click Update ChildA Only → only ChildA renders (its siblings go gray/skipped). Click Toggle Context → only ChildC (the consumer) renders; Parent is memo-skipped. The gold-check drives all three and asserts the commit log matches.
commits: — · gold: mount(all) → parent(all) → childA(only) → context(only ChildC)
symptom → fix
| symptom (what the Profiler shows) | fix | why it works |
|---|---|---|
| child renders on every parent update | React.memo(Child) + stable props |
shallow Object.is prop compare skips equal-prop renders |
| memoized child STILL re-renders | useCallback / useMemo the props |
inline fns / object literals are new refs every render — memo sees "changed" |
| one context value re-renders the world | split into N contexts, or useMemo the value |
every consumer re-renders on any Provider value change — narrow the blast radius |
| root state cascades through a huge tree | move state down to a leaf | only the owner + its descendants re-render — shrink the subtree |
| don't know why it rendered | Profiler → "Record why each component rendered" | annotates each commit with the exact reason (props/state/context/parent) |
| "I'll just memo everything" | don't — profile first | memo has a compare cost; only memo what the Profiler proves is wasted |