Unnecessary re-renders are the most common React performance problem we find in reviews, and the most commonly mis-fixed. Teams reach for React.memo and useCallback everywhere, the code gets harder to read, and the app is no faster — because the actual offender was never identified. This is the workflow we use to find re-renders systematically before touching any code.
Step 1: Prove there is a problem
Not every re-render matters. React re-rendering a component means calling its function and diffing the result; for small components this costs microseconds. The re-renders worth hunting are the ones attached to slow frames — a laggy keystroke, a janky scroll, a sluggish route change.
Start with the React DevTools Profiler, not with intuition. Record the interaction that feels slow, then look at the flame chart for the commits with the longest duration. You are looking for two patterns: a single expensive component rendering when it should not, or a wide, shallow wave of hundreds of cheap components rendering together. The first is a memoization problem; the second is usually a state-placement problem, and memoizing individual components will not fix it.
Turn on "Record why each component rendered" in the Profiler settings before you record. Each component in the flame chart will then tell you whether it rendered because of props, state, hooks, or its parent — which is most of the diagnosis done for you.
Step 2: Read the "why" honestly
The three answers you will see, and what they mean:
"The parent component rendered." The default in React: when a component renders, its children render too, unless something stops it. A wide wave of these under one state change means the state lives too high in the tree. The cheapest fix is usually not memo — it is moving the state down into the subtree that uses it, or lifting the expensive children out via the children prop so they are created by a component that does not re-render.
"Props changed." Expand the entry and look at which props. If the changed prop is an inline object, array, or arrow function — style={{...}}, onSelect={() => ...} — the value is new on every parent render even when its contents are identical. This only matters if the component is memoized or the prop feeds an effect's dependency array; that is the situation where stabilizing the value with useMemo/useCallback pays.
"Hook N changed." Count the hooks in the component's source to identify which one. A context value is a frequent culprit: every consumer of a context re-renders when the context value changes, so a single app-wide context holding { user, theme, cart, notifications } re-renders everything that reads any of it, whenever any of it changes. Split contexts by update frequency, and keep the value referentially stable with useMemo.
Step 3: Fix structurally before memoizing
Our order of operations, cheapest and most durable first:
- Move state down. If only the search box cares about the query text, the query text should not live in the page component.
- Pass children instead of rendering inside. A component that owns fast-changing state can receive its expensive subtree as
children; React reuses the same element and skips re-rendering it. - Split contexts by consumer and update rate.
- Then memoize —
React.memoon the component at the top of the expensive subtree, withuseMemo/useCallbackupstream to keep its props stable. Memoization is a contract: one unstable prop silently voids it, so verify in the Profiler that the memo actually holds.
Step 4: Re-measure and keep the receipt
Record the same interaction again and compare commit durations. If the numbers did not move, revert the change — clever code that does not change a measurement is a maintenance cost with no benefit. When the numbers did move, note the before/after in the pull request. Six months later, that note is what stops someone from "simplifying" the fix away.
A note on the React Compiler
React 19's compiler automates much of the memoization category: it analyzes components and memoizes values and elements without hand-written useMemo/useCallback. It is worth planning for — but it does not relocate misplaced state or split an overloaded context. The structural fixes in step 3 remain your job, which is exactly why we put them first: they survive the tooling shift, and hand-rolled memoization largely will not.
The habit that matters more than any individual technique: never optimize a render you have not profiled, and never keep an optimization you cannot demonstrate. Re-renders are the area of React where guesswork is most tempting and least effective — and where twenty minutes with the Profiler routinely outperforms a week of speculative refactoring.