You call setState. React doesn't just update the DOM.
It runs three separate phases, in sequence.
And conflating them is the source of most misconceptions about React performance.
The three phases:
- Render — React calls your component function
- Reconcile — React diffs the new output against the previous one
- Commit — React applies the minimum DOM changes
(Strictly speaking, there's also a Schedule phase before all of these — when setState is called, React enqueues the update and decides its priority. For most performance questions, treat it as the trigger. The three phases above are where the visible work happens.)
Most React documentation conflates them. Most React developers do too.
Once you separate them, a lot of behavior that used to be confusing starts to make sense.
The render phase
When React "renders" your component, it calls your component function.
That's it.
function UserCard({ user }) {
return <div>{user.name}</div>
}
React invokes the function with the current props.
The function returns JSX, which is just sugar for React.createElement() calls. Those calls return plain JavaScript objects — React elements.
The render phase produces a tree of these elements. People call this tree the "virtual DOM."
But it's not a DOM in any meaningful sense.
It's a description of what the DOM should look like — JS objects with type, props, and children.
Nothing rendered. Nothing painted. Nothing on screen.
The render phase is pure computation. It doesn't touch the actual DOM.
This is the part most people mean when they say "the component re-rendered."
But it's only step one.
The reconcile phase
React holds onto the previous render's output.
When you re-render, React now has two trees — the one from before, and the one you just produced.
Reconciliation is the diff between them.
React walks both trees and asks: what changed?
Different element types at the same position get the whole subtree replaced. Same element type gets props updated in place. Children with key props get matched by key, not by position.
The output is a list of the minimum DOM operations needed to transform the previous DOM into the new state.
Reconciliation is also where keys actually matter.
Without keys, React matches list children by position. With keys, React matches by identity.
This is why using array index as a key breaks lists that reorder — but that's its own post.
The commit phase
This is the only phase that touches the actual DOM.
React takes the list of DOM operations from reconciliation and applies them.
New nodes get created. Deleted nodes get removed. Changed props get updated. Moved nodes get repositioned.
Then useLayoutEffect callbacks run synchronously.
Then the browser paints.
Then useEffect callbacks run asynchronously, in the next task.
The commit phase is when users actually see something change.
Why the distinction matters
The virtual DOM isn't fast.
The real DOM isn't slow — orchestrating updates correctly is what's hard.
The virtual DOM exists so React can do the orchestration: you describe what you want, React computes the minimum operations to get there.
The win is correctness and minimal updates, not raw speed.
A "re-render" doesn't always change the DOM.
If your component produces the same tree as before, reconciliation finds no changes, and commit applies nothing.
The function still ran. The DOM didn't move.
This is what React.memo skips — the function call, not the DOM update.
useEffect runs after commit, after paint.
If you're trying to read the DOM in an effect and seeing stale values, you're probably running it during render.
If you need to read or modify the DOM before paint, that's useLayoutEffect.
The phases matter.
What actually triggers it
A component re-renders when its own state changes (via useState or useReducer), when its parent re-renders, or when a subscribed context value changes.
That's the full list.
Notably, "props changed" isn't on it.
Props change because the parent re-rendered with different props.
The parent's re-render is the trigger. The prop change is a symptom.
This distinction matters when you reach for React.memo.
You're not preventing "re-render on prop change."
You're preventing the cascade from your parent's re-render.
What this changes in practice
Honestly, not much in day-to-day code. You'll write components the same way.
What changes is how you reason about performance. When something feels slow, the question stops being "why is React re-rendering this component" and becomes "which phase is the bottleneck."
Is your function body doing heavy work during render? Memoize the computation, or move the work somewhere it doesn't run every render.
Is React rebuilding huge subtrees during reconciliation because keys aren't doing their job? Fix the keys, or wrap stable subtrees in React.memo.
Is commit applying a thousand DOM changes at once? That's usually a real-DOM problem — virtualize the list, batch the updates, or rethink the structure.
If you're on React 19, the React Compiler handles a lot of this memoization automatically. Where you'd previously reach for useMemo, useCallback, or React.memo, it now inserts equivalent caching at build time — the diagnostics still apply, but the fixes are increasingly something the compiler handles for you.
Different phases, different fixes. The "re-render" is rarely the unit worth optimizing. The phase is. And the fix is usually smaller than you'd expect.

Top comments (0)