Handbooks / React / Chapter 5

Rendering and Performance

7 pages · ~5 min✓ Reviewed

Builds on Effects and Data Fetching.

ReactPage 1

What Triggers a Re-render

1. Render vs DOM update

  • A render = React calls your component to compute JSX
  • A render can end with zero DOM changes
  • The DOM is touched only at commit

2. What triggers one

TriggerRe-render?
Own state setteryes
Parent re-rendersyes, all children
Consumed context changesyes
Props change aloneno, not a trigger
  • Child re-renders even with identical props
  • Skip it by wrapping the child in React.memo

3. Mistake: mutating state

jsx
items.push(4)  // mutated in place
setItems(items)  // Object.is same: skipped
setItems([...items, 4])  // new array: updates
Same reference = bail-out

React compares with Object.is. Always pass a new array or object.

ReactPage 2

Reconciliation and the Virtual DOM

1. Virtual DOM

  • Virtual DOM = light tree of JS objects (React elements)
  • Reconciliation diffs the new tree against the previous one
  • Only the minimal DOM changes get committed
  1. Rendernew tree
  2. Diffvs previous
  3. Commitminimal DOM changes

2. Diff Rules

Same positionResultState
Different typeTear down, remountLost
Same typeUpdate in placeKept
Key: stable idItem matchedKept
Key: array indexBreaks on reorder, insert, removeWrong
  • Changing a key on purpose resets that component's state

3. Mistake

Component defined inside a component

Each render makes a new function type, so React remounts it: state and focus are lost. Define it at module level.

4. Interview Point

  • Why O(n), not O(n^3)? Two heuristics: type check and keys, no full tree comparison
  • Type changes at same position? Old subtree torn down, state lost
  • Why is Math.random() a bad key? New key each render, so every item remounts
  • Virtual DOM faster than real DOM? No: it is overhead that avoids needless DOM writes
ReactPage 3

React.memo in Practice

1. What memo checks

  • React.memo(C) skips a render if each prop is Object.is equal
  • Guards parent-driven renders only
  • areEqual comparator: rarely needed
CauseMemo child
Parent renders, props equalskipped
New object, array or function proprenders
Own state changerenders
Consumed context changerenders

2. Stable props

  • Inline objects, arrays, functions are new each render
  • Fix with useCallback and useMemo; parent {children} JSX is new too
jsx
const Row = React.memo(function Row({ item, onSelect }) {
  return <li onClick={() => onSelect(item.id)}>{item.name}</li>;
});
const onSelect = useCallback(id => setSel(id), []);
<Row item={item} onSelect={onSelect} />

stable onSelect lets Row skip renders

3. Interview Point

  • How are props compared? Shallow, Object.is per prop
  • Why does an inline callback fail? New function identity each render
  • Does memo block state or context renders? No
  • Custom comparator? Rarely; only for a cheap, correct check
One bad prop

A single unstable prop defeats React.memo.

ReactPage 4

Memoization Trade-offs

1. What They Cache

useMemouseCallback
Cachesa computed valuea function identity
Keyed bydependency arraydependency array
Costsmemory + a compare every rendersame

2. Worth It?

  • Yes: genuinely expensive calculation
  • Yes: feeds a memoized child or an effect dependency
  • Yes: list with many items
  • No: cheap calculations or primitives

3. Cheaper First

  • Move state down into the component that needs it
  • Lift static JSX up, pass it as children: not re-created on state change
  • Measure with the React DevTools Profiler first

4. Mistakes

Don't wrap everything

Wrong or missing dependencies cause stale closures. The cache is no guarantee: React may discard it.

ReactPage 5

Code Splitting with lazy and Suspense

1. What lazy Does

  • Code splitting = bundle cut into chunks loaded on demand
  • Chunk boundary: dynamic import()
  • lazy loads the module's default export on first render
jsx
const Page = lazy(() => import('./Page'));
<Suspense fallback={<Spinner />}>
  <Page />
</Suspense>

lazy() at module top level

2. Suspense and Split Points

  • Lazy component needs a Suspense ancestor; it shows the fallback while loading
  • Nested boundaries: inner fallback covers only its part
SplitVerdict
Routesfirst
Modal, chart, editoryes, heavy
Tiny componentno, extra request

3. Mistakes

  • No Suspense above: whole root suspends, nothing new shows until the chunk loads
  • Failed chunk rejects: wrap in an Error Boundary with retry
  • lazy() inside a component: remounts every render

4. Interview Point

  • What must the module export? A default export of a component
  • No Suspense boundary? Root suspends, no fallback, no error
  • Where to split? Routes first, then heavy modals, charts, editors
  • Chunk fails? Error Boundary shows a retry
ReactPage 6

Re-render and Performance Cheat Sheet

1. Triggers and Reconciliation

CaseResult
Own state, parent render, contextre-render
New reference in stateupdate
Same reference (Object.is)bail-out
Same typeupdate, keep state
Different typeremount, state lost
  • Component defined inside a component: remounts every render
  • Index or Math.random() keys break state; use stable ids

2. Performance Order

  1. Profilermeasure first
  2. Move state down
  3. React.memostable props
  4. useMemo / useCallbackonly where needed

3. Split with lazy

jsx
const Page = lazy(() => import('./Page'))
<ErrorBoundary>
  <Suspense fallback={<Spinner />}>
    <Page />
  </Suspense>
</ErrorBoundary>

call lazy() at module top level

  • Error Boundary catches chunk load failures
  • No Suspense above it: the root suspends, nothing shows until the chunk loads

4. Interview Point

  • Render vs commit? Render calls components and diffs; commit applies the DOM changes
  • Stop a re-render? React.memo, move state down, pass static JSX as children
  • Skip memoizing? Cheap values, primitives, or no measured slowdown
  • Split a route bundle? lazy per route, inside Suspense
ReactPage 7

Check yourself

items.push(x); setItems(items); and the list never updates. Why?
  • Same reference, so Object.is says unchanged and React bails out
  • Pass a new array: setItems([...items, x])
Row is wrapped in React.memo, but the parent passes onSelect={() => pick(id)}. Does Row skip renders?
  • No: the inline function is a new reference every render, so the shallow compare fails
  • Wrap it in useCallback so the reference stays stable
A text field defined inside another component's body loses focus on every keystroke. Why?
  • Each render creates a new function type, so React unmounts and remounts it
  • Define the component at module level