CONCEPT Cited by 1 source
React re-render¶
React re-render is the cost of React re-invoking a component's render function + diffing the returned virtual-DOM tree + reconciling to the real DOM when state or props change. On hot paths (concepts/hot-path), unnecessary re-renders multiply the per- interaction cost by the component tree's depth × breadth and dominate INP.
Why components re-render (the sources)¶
- State change. A
useState/useReducerupdate triggers re-render of the component and all its descendants that don't short-circuit via memoization. - Props change. Parent re-render passes (often new-object-
identity) props to children; without
React.memoor equivalent prop-stability, children re-render. - Context change. Any component reading a context re-renders when the context value's identity changes.
useEffectside-effects. An effect that sets state triggers another render cycle; scattered effects across a large component tree multiply this.- New handler / object identity per render. An inline
onClick={() => ...}creates a fresh function each render, breakingmemocomparisons in children.
Why it's hard to contain at scale¶
- Top-down propagation — a state change in a parent re-renders the subtree by default. Memoization breaks propagation, but each memoized child needs stable props.
- Hook composition — a deeply-nested component tree with
useEffecton every level means even a stable top-level prop still re-runs many effects on each render. - Debugging is hard — React DevTools' "why did this render" is useful but requires the debugger enabled, which isn't the production shape.
Anti-patterns (named by GitHub's v1 → v2 work)¶
- Thin reusable wrapper components that exist only to share code between two views (e.g. split vs unified diff). The wrapper adds a re-render layer + state scope without adding value. v1's 8-13 components per diff line collapsed to 2 dedicated-per-view components in v2.
useEffectscattered through the tree. Defeats memoization and creates extra render cycles. GitHub's v2 restrictsuseEffectto the top level of diff files, enforced with ESLint rules.- Expensive state held by components that don't need it. v1 had commenting and context-menu state on every diff line; v2 moves it into conditionally-rendered child components that only render when the state is active (patterns/conditional-child-state-scoping).
- O(n) lookups per render. If a render reads
comments.find(c => c.line === L)across all diff lines, the per-frame cost is quadratic in line count. JavaScriptMapkeyed bypath + Lis O(1) (patterns/constant-time-state-map).
Canonical wiki instance¶
GitHub's Files-changed tab v1 → v2 cut React components rendered from ~183,504 to ~50,004 (−74 %) on the 10,000-line split-diff benchmark, while DOM nodes only shrunk 10 %. The re-render count dominated the v2 win — half the memory reduction and most of the INP improvement (450 ms → 100 ms) trace to re-render-cost reduction, not DOM-size reduction.
Fixes stack¶
Three orthogonal fixes, all visible in GitHub's v2:
- patterns/component-tree-simplification — fewer components = fewer render calls.
- patterns/conditional-child-state-scoping — expensive state lives in children that only mount when needed.
- patterns/constant-time-state-map — O(1) lookups on interaction paths replace O(n) scans.
Plus discipline (strict useEffect budget + enforced via linting)
and memoization (enabled by disciplined props + hook usage).
Related¶
- concepts/interaction-to-next-paint — the metric re-render cost dominates.
- concepts/hot-path — the code path where re-renders compound.
- concepts/window-virtualization — structural fix when the underlying list is too long to render efficiently at all.
- systems/react — the framework.
- systems/github-pull-requests — canonical wiki instance with published numbers.