Q: How does React's reconciliation algorithm work? What is the Virtual DOM and how does Fiber change things?
Answer:
Reconciliation is the process of figuring out what changed between two renders and applying the minimum set of DOM mutations. React describes the desired UI as a tree of plain objects (the Virtual DOM / "React elements"), diffs the new tree against the previous one, and emits the diff to the host environment (DOM, native, canvas).
The Diff Heuristic (O(n))
A full tree diff is O(n³). React makes it linear by leaning on two assumptions:
- Two elements of different types produce different trees. A
<div>swapped for a<span>is treated as "tear down and rebuild", not "compare children". - Stable keys identify siblings across renders. Without keys, React diffs by index; with keys, it matches by identity.
Old tree New tree
<ul> <ul>
<li key="a"/> match→ <li key="b"/>
<li key="b"/> <li key="a"/>
<li key="c"/>
With keys, React detects: keep a and b, just move them, insert c. Without keys (or with key={index}), it would re-render each <li> and rebuild internal state.
Same-Type Component Update
<Counter count={1} /> → <Counter count={2} />
React keeps the instance, updates props, calls the component function, diffs its output. Local state (useState) is preserved.
Element-Type Change Tears Down State
{loading ? <Spinner/> : <Form/>}
When loading flips, React unmounts Spinner (running its cleanups) and mounts a fresh Form. Any state inside Form from a previous show is lost — it's a brand new component instance.
Why Keys Matter So Much
Wrong keys are the most common subtle React bug. Using array index as key in a reorderable list causes:
- Inputs lose their values when items are inserted at the top.
- Animations attach to the wrong element.
useEffectcleanup runs for the wrong identity.
[!NOTE] A good key is a stable, unique identifier of the underlying data. Database IDs are ideal. Indexes are fine only if the list is append-only and items have no internal state.
Fiber — Incremental Reconciliation
Prior to React 16, reconciliation was a synchronous recursive walk that blocked the main thread for large trees. Fiber rewrote this into a cooperative, interruptible scheduler.
Key ideas:
- Each component instance is a fiber node in a linked-list tree.
- Work is split into units that can be paused, resumed, or discarded.
- A double-buffered tree ("current" and "work-in-progress") lets React build the next render without disturbing the current one.
- The scheduler assigns priorities — urgent updates (input, hover) preempt low-priority ones (data fetch results, transitions).
Render Phase (interruptible)
┌──────────────────────────────────────────┐
│ begin work on each fiber │
│ build work-in-progress tree │
│ collect side-effects in a list │
└──────────────────┬───────────────────────┘
│
v
Commit Phase (synchronous, atomic)
┌──────────────────────────────────────────┐
│ apply DOM mutations │
│ run layout effects (useLayoutEffect) │
│ schedule passive effects (useEffect) │
└──────────────────────────────────────────┘
Concurrent Features Build on Fiber
useTransitionmarks a state update as non-urgent — React can pause it to keep input responsive.useDeferredValuelets a heavy subtree lag behind a typing-driven input.Suspenselets a component "throw" a promise; React renders fallback content until the promise resolves, then continues.
Render vs Commit
- The render phase calls your function components — it may run multiple times, can be aborted, must be pure.
- The commit phase applies the result. Side effects belong here (
useEffect,useLayoutEffect).
Common Perf Pitfalls That Fight the Reconciler
- New inline objects/functions every render that are then passed as props with
useMemo-derived deps (defeats memoization). - Conditional rendering that swaps component types (
AvsB) when re-using a single component with a prop would preserve state. - Putting whole-app state in a Context provider's
valuerecreated each render — every consumer re-renders.
What Reconciliation Does Not Do
- It does not minimize re-renders — it minimizes DOM mutations. Components still call your function every render unless memoized.
- It does not deduplicate state updates — multiple
setStatecalls in the same event are batched, but state is not "diffed" before commit.