Back to React
React
medium
mid

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.

7 min read·~5 min to think through

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):

  1. Elements of different types produce different trees. A <div> becoming a <span> triggers a full unmount/remount of the subtree.
  2. 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.
tsx
// before
<div className="a" />
// after
<div className="b" />
// → mutate className, keep node

Different type:

  • Unmount the old subtree (run cleanups).
  • Mount the new subtree (run effects).
tsx
// before
<div><Child /></div>
// after
<span><Child /></span>
// → unmount entire div+Child, mount span+new Child

Component 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.

tsx
// 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 B

Two phases

Reconciliation happens in two phases (since React 16/Fiber):

  1. Render phase (interruptible): builds a 'work-in-progress' fiber tree. No DOM changes yet.
  2. 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.

Senior engineer discussion

Senior framing: reconciliation is the contract between your render output and the DOM. The fewer surprises (stable types, stable keys, stable structure), the more efficient. Most React perf optimizations are really 'make reconciliation cheaper'.

Related questions