Rendering Performance
Why a component re-renders
Section titled “Why a component re-renders”There are exactly three reasons a component re-renders:
- Its own state changed (a setter was called).
- Its parent re-rendered — by default, when a component renders, React re-renders all of its children.
- A context it consumes changed value.
That second one surprises people: a parent re-rendering re-renders the whole subtree below it, even children whose props did not change. React does this because it cannot know, without checking, whether a child’s output depends on something that changed.
flowchart TB app["App re-renders (state changed)"] --> header["Header re-renders"] app --> main["Main re-renders"] main --> list["List re-renders"] list --> item1["Item re-renders"] list --> item2["Item re-renders"] note["Every descendant re-renders, even if its props are unchanged"]
Re-rendering is not free, but it is usually cheap — remember reconciliation produces zero DOM changes when output is unchanged. The problem only appears when a large subtree, or an expensive component, re-renders needlessly and often.
Fix #1: keep state low and local
Section titled “Fix #1: keep state low and local”The best fix is structural. If a piece of state lives high in the tree but only one small branch uses it, every state change re-renders the whole tree. Move the state down to the component that actually needs it, so a change re-renders only that small branch.
// ❌ Search text lives in App, so typing re-renders the entire app.function App() { const [query, setQuery] = useState(""); return ( <> <SearchBox query={query} onChange={setQuery} /> <HugeExpensiveDashboard /> {/* re-renders on every keystroke! */} </> );}
// ✅ Move the state into SearchBox; typing re-renders only SearchBox.function App() { return ( <> <SearchBox /> <HugeExpensiveDashboard /> {/* never re-renders while typing */} </> );}A related trick: lift content by composition. If a fast-changing parent must wrap a slow child, pass the slow child in as children — children passed as props do not re-render just because the parent’s state changed.
Fix #2: stop propagation with React.memo
Section titled “Fix #2: stop propagation with React.memo”When you cannot move state and a child is genuinely expensive, wrap it in React.memo. A memoized component skips re-rendering when its props are shallow-equal to the previous render — it becomes a wall that a parent re-render does not cross.
const ExpensiveList = React.memo(function ExpensiveList({ items }) { // Only re-renders when `items` (by reference) actually changes. return items.map((it) => <Row key={it.id} item={it} />);});The catch: React.memo only helps if the props are actually stable. Pass a fresh inline object, array, or function as a prop and the shallow comparison always fails — which is why memoization often needs useMemo/useCallback on the props too (next lesson).
Measure first: the Profiler
Section titled “Measure first: the Profiler”Do not guess where the slowness is. The React DevTools Profiler records a session and shows you exactly which components rendered, how often, and how long each took — and why each rendered. Optimize the component the profiler points at, not the one you suspect.