What Is the Virtual DOM?
The virtual DOM (VDOM) is a lightweight, in-memory representation of the real DOM. Instead of touching the browser DOM directly on every state change — an expensive operation — React keeps a JavaScript tree of objects describing what the UI should look like. When state changes, React produces a new virtual DOM tree, compares it with the previous one, and only applies the minimal set of real DOM mutations needed to bring the two trees into sync.
This comparison step is called reconciliation.
// React builds a virtual DOM tree from JSX
const element = (
<ul>
<li>Alpha</li>
<li>Beta</li>
</ul>
);
// Internally this becomes a plain JS object:
// { type: 'ul', props: { children: [{ type: 'li', ... }, ...] } }
The payoff: batch updates, predictable re-render semantics, and a layer of indirection that makes server-side rendering and React Native possible without changing application code.
The Reconciliation Algorithm — Two Core Heuristics
React's diffing algorithm runs in O(n) time (not the theoretical O(n³) for arbitrary trees) by relying on two assumptions:
- Different element type → tear down and rebuild. If a
<div>is replaced by a<span>, React destroys the entire subtree — including component state — and mounts a fresh one. This is why wrapping a component in a container of a different type during a conditional render resets all its children. - Same element type → update in place. When the type stays the same, React keeps the existing DOM node and only updates the attributes or props that changed. For class components it calls
componentDidUpdate; for function components it re-invokes the function and reconciles the output.
// Heuristic 1 in action — changing the wrapper type resets state
function Tabs({ isAdmin }) {
return isAdmin
? <div><Counter /></div> // Counter unmounts when isAdmin flips
: <section><Counter /></section>;
}
// Fix: keep the type consistent
function Tabs({ isAdmin }) {
return <div>{isAdmin ? <AdminPanel /> : <GuestPanel />}</div>;
}
Fiber — React's Reconciliation Engine
React Fiber (introduced in React 16) is a complete rewrite of the reconciliation engine. The key idea: break rendering work into small units called fibers (one per component) that can be paused, aborted, or restarted.
Before Fiber, reconciliation was a synchronous recursive walk. A large tree could block the main thread for tens of milliseconds — causing dropped frames. Fiber makes the work interruptible, which unlocks:
- Concurrent rendering — React 18's
startTransition,useDeferredValue, and Suspense all depend on Fiber's ability to pause and resume. - Priority scheduling — urgent updates (user input) can preempt lower-priority work (a background data fetch re-render).
- Error boundaries — Fiber tracks which subtree threw, so React can unmount only that subtree and display a fallback.
The reconciliation pass produces a work-in-progress tree. React only commits that tree to the real DOM after the entire diff is complete, keeping the UI consistent at all times.
Keys — The Third Heuristic
When reconciling lists, React needs to match old children to new ones. Without hints it falls back to index-based matching, which causes subtle bugs when items are reordered or inserted.
The key prop tells React the stable identity of each list item:
// Bad — index-based matching breaks on reorder/insert
{items.map((item, i) => <Row key={i} data={item} />)}
// Good — stable identity
{items.map(item => <Row key={item.id} data={item} />)}
A key change is equivalent to changing the element type: React unmounts the old component (losing its state) and mounts a fresh one. This is occasionally useful as an intentional reset trick — passing a new key to force a child to reinitialise.
Render Phase vs Commit Phase
Reconciliation has two distinct phases:
| Phase | What happens | Can be interrupted? |
|---|---|---|
| Render | React calls component functions, diffs the output | Yes (Concurrent Mode) |
| Commit | DOM mutations, useLayoutEffect, useEffect | No — runs synchronously to completion |
Side effects must live in the commit phase (useEffect, useLayoutEffect). Running them during the render phase would break concurrent rendering because that phase can be replayed.
Hydration
In server-side or statically rendered React, the browser receives pre-built HTML. Hydration is the process of attaching event handlers and initialising component state on top of that existing markup — without discarding and rebuilding the DOM. React walks the server-rendered tree and the client VDOM tree in parallel; a mismatch (hydration error) causes React to fall back to a full client-side re-render for the affected subtree, which is expensive and visible to users.
Finding Bottlenecks with React DevTools Profiler
The React DevTools Profiler records which components rendered, how long each took, and why they rendered (prop change, state change, context change, hooks). Key workflow:
- Open DevTools → Profiler tab → click Record.
- Interact with the slow part of the UI, then stop recording.
- Inspect the flame graph for unexpectedly wide bars — components that re-rendered but should have bailed out.
- Check the "Why did this render?" tooltip. If it says "parent re-rendered" for a pure display component, that's a
React.memocandidate.
// Enable why-did-you-render in development for automated warnings
import whyDidYouRender from '@welldone-software/why-did-you-render';
if (process.env.NODE_ENV === 'development') {
whyDidYouRender(React, { trackAllPureComponents: true });
}
Rule of thumb: optimise reconciliation at the data boundary — keep state as low as possible in the tree and memoize only after the Profiler confirms a problem. Premature memoization adds overhead without benefit.