How does React's reconciliation process update the DOM efficiently?
Reconciliation is React's algorithm for diffing two element trees and computing the minimum DOM mutations to transform the previous tree into the new one. Heuristics: (1) different element types remount the whole subtree; (2) same type diffs attributes/children; (3) keys identify list items across renders. O(n) where n = tree size, vs naive O(n³) for general tree diff. Fiber makes the walk interruptible.
Reconciliation is how React turns 'here's the new UI' into 'here are the minimum DOM changes to get there'.
The problem
You can't naively diff two trees efficiently — general tree-diff is O(n³). React makes two assumptions that turn it into O(n):
- Elements of different types produce different trees. A
<div>becoming a<span>triggers a full unmount/remount of the subtree. - Keys identify list children across renders. Without keys, React falls back to index matching, which works for static lists but breaks for inserts/reorders.
The algorithm
When React renders, it compares the new element tree to the previous one node-by-node:
Same type:
- Keep the DOM node.
- Update changed attributes.
- Recurse on children.
// before
<div className="a" />
// after
<div className="b" />
// → mutate className, keep nodeDifferent type:
- Unmount the old subtree (run cleanups).
- Mount the new subtree (run effects).
// before
<div><Child /></div>
// after
<span><Child /></span>
// → unmount entire div+Child, mount span+new ChildComponent type:
- Same component → keep instance, call the function with new props.
- Different component → unmount old, mount new.
Children diffing
For each child position, React compares old vs new:
Without keys: position-based. Insert/delete in the middle causes cascading remounts of everything after.
With keys: identity-based. React matches old and new by key, preserves state on matched, mounts/unmounts only what changed.
// before
<ul><li key="a">A</li><li key="b">B</li></ul>
// after — insert C at the beginning
<ul><li key="c">C</li><li key="a">A</li><li key="b">B</li></ul>
// React: mount C, keep A and BTwo phases
Reconciliation happens in two phases (since React 16/Fiber):
- Render phase (interruptible): builds a 'work-in-progress' fiber tree. No DOM changes yet.
- Commit phase (synchronous): applies all DOM changes in one shot, runs lifecycle methods, fires useLayoutEffect.
The split lets React pause/resume render work without leaving the DOM in an inconsistent state.
Effect tags
During reconciliation, React tags each fiber with what to do at commit:
- Placement: insert new DOM node.
- Update: change attributes.
- Deletion: remove DOM node.
- PlacementAndUpdate: move + change.
At commit, React walks the effect list and applies each.
Why keys matter
Without keys (or with index keys), React matches children by position. If you delete the first item from a list of 100, React thinks every position has changed. With ids as keys, React knows exactly which one to remove.
Performance characteristics
- O(n) where n = total elements in the tree.
- Each fiber's work is bounded by its children count.
- DOM mutations are batched — applied in a single synchronous commit.
- Browsers only repaint once per commit.
Where it can be slow
- Massive trees (10k+ components rendered): even O(n) hurts. Use virtualization.
- Different types in stable positions: forces unmount/remount unnecessarily. Stabilize structure.
- Bad keys: index keys for reordered lists cause spurious work.
What developers control
- Don't change element types unnecessarily:
{cond ? <A/> : <B/>}remounts;<A {...(cond ? {} : {alt: true})}/>doesn't. - Use stable keys: ids, not array indices for mutable lists.
- Avoid inline component definitions:
function Wrapper() { const Sub = () => ...; return <Sub/>; }— Sub's type changes every render, full remount.
Senior framing
Reconciliation is a heuristic-driven O(n) tree diff that works because the heuristics (different types = different trees, keys = identity) match how real UI changes. Understanding the heuristics is what makes you write performant React: keys, stable element types, lift state appropriately.
Follow-up questions
- •Why is keying lists important for state preservation?
- •What's the difference between render phase and commit phase?
- •How does Fiber make reconciliation interruptible?
Common mistakes
- •Defining a component inside the parent's render — new type every render, full remount.
- •Using array index as key in a mutable list — state ends up on wrong rows.
- •Frequently swapping element types (div vs span) — unnecessary remounts.
Performance considerations
- •Reconciliation cost scales with the number of components React has to consider, not the DOM tree size. Memo and stable keys reduce the considered set. Virtualization is the answer when both are huge.
Edge cases
- •Fragments don't add reconciliation overhead but their children are diffed in place.
- •Portals reconcile normally; the DOM target changes but the tree relationship is preserved.
- •Context providers reconcile like normal components but propagate updates to consumers separately.
Real-world examples
- •Every React app's update cycle. Visible when you change a key prop deliberately to force a remount, or when a deeply-nested type swap unmounts an animation in progress.