Skip to content

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)

  1. State change. A useState / useReducer update triggers re-render of the component and all its descendants that don't short-circuit via memoization.
  2. Props change. Parent re-render passes (often new-object- identity) props to children; without React.memo or equivalent prop-stability, children re-render.
  3. Context change. Any component reading a context re-renders when the context value's identity changes.
  4. useEffect side-effects. An effect that sets state triggers another render cycle; scattered effects across a large component tree multiply this.
  5. New handler / object identity per render. An inline onClick={() => ...} creates a fresh function each render, breaking memo comparisons 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 useEffect on 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.
  • useEffect scattered through the tree. Defeats memoization and creates extra render cycles. GitHub's v2 restricts useEffect to 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. JavaScript Map keyed by path + L is 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:

  1. patterns/component-tree-simplification — fewer components = fewer render calls.
  2. patterns/conditional-child-state-scoping — expensive state lives in children that only mount when needed.
  3. 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).

Last updated · 200 distilled / 1,178 read