Render and commit: the two phases of an update
Every React update runs through two phases with opposite rules — an interruptible render phase that builds the work-in-progress tree and must stay pure, followed by a single uninterruptible commit pass that applies every DOM change at once so the user never sees a half-updated screen.
Render and commit: the two phases of an update
The last lesson described fiber as the structure that makes reconciliation pausable — a loop over units of work instead of one uninterruptible recursive call. This lesson is about what that loop is actually for, split into the two phases every update passes through: render, where React figures out what changed, and commit, where it actually changes the DOM. Those two phases don't just run one after another — they run under opposite rules, and the reason for that split is the subject of this whole lesson.
Render: interruptible, so it must be pure
Render is the phase covered mechanically in the last lesson: React walks the work-in-progress fiber tree via beginWork/completeWork, calling your component functions to find out what they return, diffing the results against the current tree, and marking each fiber that needs a DOM change with an effect flag describing what kind of change. Nothing touches the real DOM yet — this phase only produces a fully-updated WIP tree plus a list of pending changes.
Because it's built from a loop over small units of work, render can be interrupted between any two fibers. React can pause it to let a higher-priority update run first, and — this is the part that surprises people — it can throw the entire partial WIP tree away and start the render over from scratch, if something about the update changed while it was mid-flight. Both of those are only safe because of one rule: render must be pure.
"Pure" here means concretely: no DOM mutation, no network request, no mutating a variable outside the component, no subscription side effect — nothing during render that the outside world would notice happening, because render might run more than once for the same update, or might run and then be discarded entirely. A component function is supposed to be a function of its props and state that computes an output; anything it does beyond that computation is a bug waiting to surface the moment React decides to re-run or abandon that render.
This is exactly what React's StrictMode double-invocation is designed to catch. In development, StrictMode deliberately calls each component function twice during render (throwing away one of the results) specifically to surface renders that aren't actually pure — if calling a function twice produces an observably different or broken result, that function was depending on render running exactly once, which was never a guarantee React made.
Commit: one synchronous, uninterruptible pass
Once render has finished computing the complete set of changes — the whole WIP tree is built and every fiber that needs a DOM change is flagged — React moves to commit. Commit is the opposite of render in every relevant way: it is synchronous and cannot be interrupted. React walks the flagged fibers and applies every DOM mutation in one uninterrupted pass, and only after that pass finishes does control return to the browser.
That's the point of the split. Render's output is just data — a tree and a list of intended changes — and discarding data mid-computation is invisible to the user. But a DOM mutation is not invisible: the moment React inserts one node, the DOM is in a new state, visible the instant the browser gets a chance to paint. If commit could be interrupted partway through applying a batch of changes, the browser could paint a frame with half the update applied — some nodes updated, others still stale — a torn, incoherent screen. Making commit one uninterruptible pass is what guarantees every paint shows either the fully old UI or the fully new one, never something in between.
Commit's three sub-phases
Commit itself runs in three ordered sub-phases, all inside that one uninterruptible pass:
- Before mutation — a bookkeeping pass over the flagged fibers before any DOM change happens; this is where React captures things it needs to read from the old DOM before it disappears (for example, scroll position, for
getSnapshotBeforeUpdate-style needs). - Mutation — the actual DOM surgery: inserting new nodes, updating attributes and text, removing nodes that no longer belong. This is the sub-phase that triggers the real work covered in the rendering engine module — every node this phase touches is a candidate for the browser's next reflow and repaint.
- Layout — runs after the DOM has the new structure but before the browser has painted it: this is when
useLayoutEffectcallbacks fire and when refs get attached to their final DOM nodes.
By the time all three sub-phases finish, the WIP tree has become the current tree (the double-buffer swap from the last lesson) and the DOM fully matches it. Only then does control return to the browser, which schedules the actual paint.
Effect timing: useLayoutEffect vs useEffect
The layout sub-phase is why React gives you two different effect hooks with deliberately different timing:
// useLayoutEffect: runs synchronously during commit's layout sub-phase,
// BEFORE the browser paints. Use it when you need to measure or mutate
// layout without the user ever seeing an intermediate frame.
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
if (height > maxHeight) {
setTooTall(true); // triggers a re-render, still before paint
}
}, [children]);
// useEffect: scheduled to run AFTER commit and after the browser paints.
// Use it for anything that doesn't need to block the frame — subscriptions,
// logging, fetching data to display later.
useEffect(() => {
const subscription = source.subscribe(setValue);
return () => subscription.unsubscribe();
}, [source]);useLayoutEffect runs synchronously, inside commit, before the browser paints — so if it measures the DOM and changes state in response, that change is folded in before anything reaches the screen, avoiding the one-frame flash you'd get from measuring after paint and correcting a moment later. The cost is that it blocks the frame: the browser cannot paint until every useLayoutEffect in the commit has finished running, so heavy work here directly delays the frame the same way any main-thread work does — the same budget from the frame and the 16ms budget applies.
useEffect (the "passive" effect) is deliberately deferred: React schedules it to run after commit and after the browser has painted, using the same kind of scheduled-task mechanism the event loop's rendering step reserves for work that doesn't need to block a frame. That deferral is the entire reason useEffect exists as a separate hook — most effects (subscriptions, logging, fetching) have no reason to hold up a paint the user is waiting to see, so React lets the frame go out first and runs them right after.
What this sets up
Render and commit are the two phases; what actually lives inside the WIP tree's fibers between renders — where useState's value goes, why hooks have to be called in the same order every time — is the next question. Hooks: state on the fiber picks that up directly, using the memoizedState field this lesson and the last one both mentioned in passing.
Go deeper
- React docs — Render and Commit — React's own walkthrough of the same two phases, with the browser-painting diagram this lesson's Mermaid chart is built from.
- React docs — useLayoutEffect — The official timing guarantees and the explicit warning to prefer useEffect unless you specifically need to measure layout before paint.
- React docs — StrictMode — Why StrictMode double-invokes render in development, and the exact list of side effects it's designed to surface.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- What does the render phase produce, and why is it safe for React to interrupt or discard it mid-way?
- Give a concrete example of a side effect that should never happen during render, and explain what breaks if React re-runs or discards that render.
- Why does StrictMode call component functions twice in development? What kind of bug is that designed to catch?
- Why must the commit phase run as a single synchronous, uninterruptible pass rather than being chunked like render?
- Name commit's three sub-phases in order and say what each one is responsible for.
- Contrast useLayoutEffect and useEffect: when does each run relative to the browser's paint, and what does each cost or save as a result?
- State in one sentence why render's rules and commit's rules are opposite, tying it back to what each phase's output actually is.