awesome-everything RU
↑ Back to the climb

Browser & Frontend Runtime

Render phase purity and commit phase sub-steps

Crux Render must be pure because React 18 may replay it; commit is synchronous, atomic, and has three sub-steps — before-mutation, mutation, and layout — each with specific APIs and failure modes.
Your altitude — climbing toward senior
ZeroJuniorMiddleSenior
You are at middle altitude — in the sky
◷ 13 min

A component that writes to a global counter on every render works fine with React 17. With React 18 in StrictMode, that counter doubles. The difference is not a bug — it is React telling you the render phase can replay your function, and side effects there will run more than once.

Render phase: interruptible, must be pure. The render phase walks the workInProgress tree, calls each function component, and runs reconciliation. Because React 18 can pause this phase, resume it, or discard it and start over, your component function may be called more than once for a single visible update — and a paused render may sit half-finished while the user interacts. This is why a render must be pure: no side effects, no mutating external variables, no DOM reads or writes, no network calls in the function body. A side effect in render runs an unpredictable number of times. React’s StrictMode deliberately double-invokes components in development precisely to surface impure renders early. The disciplined place for side effects is useEffect (after commit) or an event handler (outside render entirely).

Commit phase: atomic, synchronous, three sub-steps. Once the workInProgress tree is fully built, commit runs in one uninterruptible block. It has three sub-steps:

  1. Before-mutation: React reads any pre-mutation state. This is where getSnapshotBeforeUpdate and the cleanup snapshot for useLayoutEffect happen.
  2. Mutation: React applies every DOM change — insertions, deletions, attribute updates, text changes — driven by the effect flags it tagged onto fibers during render.
  3. Layout: React runs useLayoutEffect callbacks and updates refs, synchronously, before the browser paints.

Because commit is synchronous and uninterruptible, a huge tree with thousands of mutations produces one long task — commit is the part of React you cannot time-slice, so the way to keep it short is to keep the number of mutations small (virtualisation, memoisation).

Commit phase timeline
before-mutationgetSnapshotBeforeUpdate · useLayoutEffect cleanup snapshot
mutationDOM insertions · deletions · attribute updates · text changes
layoutuseLayoutEffect · ref updates → before browser paints
after paintuseEffect (passive) → separate async task

useLayoutEffect vs useEffect. The two differ in when they run relative to paint. useLayoutEffect fires synchronously in the commit phase’s layout step — after DOM mutation, before the browser paints. useEffect (a “passive effect”) fires asynchronously after paint, on a later task. The rule: anything that must run before the user sees the frame — measuring a just-mutated DOM node and adjusting layout to prevent a visible flash — belongs in useLayoutEffect. Everything else — data fetching, subscriptions, logging — belongs in useEffect, because doing it in useLayoutEffect delays paint. Overusing useLayoutEffect is a real INP regression: every layout effect is synchronous commit-phase work that pushes the paint later.

Common mistake

A useLayoutEffect that reads getBoundingClientRect then writes a style triggers a synchronous layout inside commit — a forced reflow. The browser must recompute layout mid-commit to answer the getBoundingClientRect call, then React writes a style, then the browser must recompute again before painting. This is the commit-phase equivalent of a layout thrash, and it is invisible in normal DevTools unless you look at commit timing.

Quiz

Why must a function component's body be pure (no side effects)?

Order the steps

Order the sub-steps of the commit phase, from the moment the workInProgress tree is finished.

  1. 1 Before-mutation: read pre-mutation state (getSnapshotBeforeUpdate)
  2. 2 Mutation: apply DOM insertions, deletions, updates
  3. 3 Swap: workInProgress tree becomes the current tree
  4. 4 Layout: run useLayoutEffect, update refs (before paint)
  5. 5 Browser paints; passive effects (useEffect) run after
Quiz

You need to measure a DOM node's size right after React updates it and before the user sees the frame. Which API do you use?

Recall before you leave
  1. 01
    Why does React StrictMode double-invoke component functions in development?
  2. 02
    List the three sub-steps of the commit phase in order.
  3. 03
    What is the failure mode of overusing useLayoutEffect?
Recap

The render phase calls your component functions and may do so multiple times per visible update in React 18’s concurrent mode — so the function body must be pure. Any side effect there (writing to a global, mutating a ref, fetching data) runs an unpredictable number of times. The commit phase is the opposite: one synchronous, uninterruptible block with three ordered steps. Before-mutation reads snapshots. Mutation applies every DOM change React tagged during render. Layout runs useLayoutEffect and updates refs before the browser paints. Passive useEffect hooks are separate and fire after paint. The commit phase cannot be time-sliced, so keeping its mutation count small is the primary lever for commit performance.

Connected lessons
appears again in143
Continue the climb ↑Reconciliation: diffing heuristics and the key trap
shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.