How React's Virtual DOM Works Under the Hood
A step-by-step mental model — from the problem it solves to the render → diff → commit lifecycle
The Problem: Direct DOM Manipulation is Expensive
Before React, building interactive UIs meant talking directly to the browser's DOM — the internal tree of objects the browser maintains to represent your webpage.
That sounds reasonable, until you realise how costly DOM operations actually are.
Every time you touch the DOM — add a node, change a class, update some text — the browser may trigger a chain of expensive operations:
Style recalculation — figuring out which CSS rules apply
Layout (reflow) — computing the size and position of every affected element
Paint — filling pixels on screen
Composite — assembling layers and sending them to the GPU
Do this once or twice? No problem. Do this dozens of times per second in response to user actions, timers, or data fetches? You get janky, slow UIs that drop frames and frustrate users.
The core issue isn't that the DOM is bad — it's that frequent, uncoordinated updates to the real DOM are brutally expensive.
Real DOM vs Virtual DOM
Before going further, let's establish what both of these actually are.
The Real DOM
The Real DOM (Document Object Model) is the browser's live, in-memory representation of your HTML page as a tree of objects. When you write:
<div class="card">
<h2>Hello</h2>
<p>World</p>
</div>
The browser builds a tree of actual DOM nodes — heavy objects with hundreds of properties each, connected to rendering pipelines, event systems, and layout engines. Updating them triggers the chain of work described above.
The Virtual DOM
The Virtual DOM is React's answer: a lightweight JavaScript object that describes what the DOM should look like — without being the DOM itself.
Think of it as a plain JS object tree:
{
type: "div",
props: { className: "card" },
children: [
{ type: "h2", props: {}, children: ["Hello"] },
{ type: "p", props: {}, children: ["World"] }
]
}
No browser APIs. No rendering pipelines. Just a plain data structure — cheap to create, cheap to compare, cheap to throw away and rebuild.
| Real DOM | Virtual DOM | |
|---|---|---|
| Lives in | Browser | JavaScript memory |
| Cost to create | High | Very low |
| Cost to update | High (triggers reflow/paint) | Negligible |
| Queryable by browser | Yes | No |
| Used for rendering | Yes (directly) | No (describes what to render) |
The Virtual DOM is never displayed itself — it's a plan that React uses to figure out the minimum set of changes it needs to make to the Real DOM.
Step 1 — Initial Render
When your React app first loads, here's what happens:
1. You write JSX
function App() {
return (
<div className="card">
<h2>Hello</h2>
<p>World</p>
</div>
);
}
JSX isn't HTML — it's syntactic sugar that Babel compiles into React.createElement() calls:
React.createElement("div", { className: "card" },
React.createElement("h2", null, "Hello"),
React.createElement("p", null, "World")
)
2. React builds a Virtual DOM tree
These createElement calls produce a plain JS object tree — the Virtual DOM. React holds this in memory.
3. React commits to the Real DOM
React walks the Virtual DOM tree and creates actual DOM nodes for each element. This is the only time React creates the full structure from scratch.
JSX
│
▼
React.createElement() calls
│
▼
Virtual DOM tree (JS objects)
│
▼
Real DOM (browser renders it)
After this initial render, React keeps the Virtual DOM tree in memory. It will use it as the "before" snapshot for the next update.
Step 2 — State or Props Change Triggers Re-render
Something changes — a user clicks a button, a fetch request completes, a timer fires. This calls setState (or a state setter from useState). React marks that component as "needs re-rendering" and schedules an update.
Here's a concrete example:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
When the button is clicked, setCount(1) is called. React now knows this component's output has potentially changed and needs to figure out what (if anything) needs updating in the Real DOM.
Step 3 — A New Virtual DOM Tree is Created
React re-runs the component function with the new state. This produces a brand-new Virtual DOM tree describing what the UI should now look like:
Old Virtual DOM (count = 0):
{
type: "div",
children: [
{ type: "p", children: ["Count: 0"] },
{ type: "button", children: ["Increment"] }
]
}
New Virtual DOM (count = 1):
{
type: "div",
children: [
{ type: "p", children: ["Count: 1"] }, // ← changed
{ type: "button", children: ["Increment"] }
]
}
Creating this new tree is cheap — it's just JavaScript objects. No browser APIs were touched.
Step 4 — Diffing (Reconciliation)
Now React has two trees: the old Virtual DOM (what the Real DOM currently looks like) and the new Virtual DOM (what it should look like).
React's job is to compare them and find the differences. This process is called diffing or reconciliation.
Old VDOM tree New VDOM tree
───────────── ─────────────
div div
/ \ / \
p button p button
│ │
"Count: 0" "Count: 1"
↑
CHANGED
React walks both trees simultaneously, node by node, comparing them. When it finds a difference, it records a "patch" — a description of the minimal DOM operation needed.
How React makes diffing fast
A naive comparison of two trees is O(n³) — far too slow for real applications. React uses a set of practical heuristics that bring this down to O(n):
Heuristic 1 — Different type = full replace
If a node changes type (e.g. <div> becomes <span>), React doesn't try to reconcile the subtree. It throws the old subtree away and builds a new one from scratch.
Old: <div>...</div>
New: <span>...</span>
→ Destroy old subtree, create new one
Heuristic 2 — Same type = update in place
If the type is the same, React updates only the changed attributes/props and recurses into children.
Old: <p className="old">Hello</p>
New: <p className="new">Hello</p>
→ Update only the className attribute
Heuristic 3 — Keys help reconcile lists
When diffing lists of children, React uses key props to match old and new nodes. Without keys, React assumes children are positionally matched — which can cause it to re-render or even re-create items unnecessarily. With keys, React can identify that a list item moved vs was added or removed.
// Without keys — React may mis-match items
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
// With keys — React correctly tracks each item
<ul>
<li key="apple">Apple</li>
<li key="banana">Banana</li>
</ul>
Step 5 — Minimal Updates Applied to the Real DOM
After diffing, React has a small, precise list of changes — the "patch". In our counter example:
PATCH:
- Update text content of <p> node from "Count: 0" to "Count: 1"
React now applies only these changes to the Real DOM in a single, batched operation. The <button> node? Untouched. The <div> wrapper? Untouched.
Old Real DOM:
<div>
<p>Count: 0</p> ← needs update
<button>…</button> ← unchanged
</div>
After patch:
<div>
<p>Count: 1</p> ← updated
<button>…</button> ← untouched
</div>
The browser only reflows and repaints the part of the page that actually changed. This is where the performance gain lives.
Why This Approach Improves Performance
Let's summarise why the Virtual DOM approach is faster than naive direct DOM manipulation:
1. Batching. React collects all state changes within an event cycle and applies them together in a single DOM update pass — rather than one DOM mutation per state change.
2. Minimal operations. Only nodes that actually changed are touched. If 1 out of 1,000 list items changes, only 1 DOM node is updated.
3. Cheap "planning" phase. The diff happens entirely in JavaScript memory — fast, with no browser rendering involvement. The expensive browser work only happens at the very end, once React knows exactly what to change.
4. Avoiding unnecessary work. If the new Virtual DOM is identical to the old one (no actual changes), React can bail out entirely without touching the Real DOM at all.
It's worth noting: the Virtual DOM isn't "faster than the DOM" in any absolute sense. Directly updating the single DOM node that changed would be faster than Virtual DOM diffing. The Virtual DOM's value is that it makes it practical to write declarative UI code without you having to manually track what changed — and React handles the optimisation automatically.
The Full React Lifecycle: Render → Diff → Commit
Here's the complete mental model, end to end:
┌─────────────────────────────────────────────────────────┐
│ TRIGGER │
│ Initial mount / setState / props change │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ RENDER PHASE │
│ │
│ 1. Run component functions with new state/props │
│ 2. Produce new Virtual DOM tree (JS objects only) │
│ 3. Diff new tree against old tree (reconciliation) │
│ 4. Compute minimal list of DOM changes (the patch) │
│ │
│ ✓ Pure JavaScript — no browser APIs touched │
│ ✓ Can be interrupted or paused (React 18+) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ COMMIT PHASE │
│ │
│ 5. Apply the patch to the Real DOM │
│ 6. Run effects (useEffect, lifecycle methods) │
│ 7. Browser reflows/repaints only changed parts │
│ │
│ ✓ Synchronous — all DOM mutations happen at once │
│ ✓ Minimum browser work possible │
└─────────────────────────────────────────────────────────┘
│
▼
Updated UI on screen
The Render phase is pure and side-effect free — React figures out what to do. The Commit phase is where React actually does it — touching the Real DOM exactly as much as needed, and no more.
Quick Reference: Key Concepts
| Concept | What it means |
|---|---|
| Virtual DOM | Lightweight JS object tree describing the UI |
| Reconciliation | Comparing old and new Virtual DOM trees |
| Diffing | The algorithm React uses to find differences |
| Patch | The minimal list of Real DOM changes needed |
| Render phase | Pure JS computation — diff and plan |
| Commit phase | Applying the patch to the Real DOM |
key prop |
Helps React correctly match list items across renders |
Summary
React's Virtual DOM is an elegant solution to a real problem: browsers are slow when you update the DOM frequently and carelessly. By maintaining a lightweight JS mirror of the DOM, React can:
Receive state changes and re-render components into a new Virtual DOM tree
Diff the new tree against the old one to find exactly what changed
Commit only the minimum necessary mutations to the Real DOM
The result is a programming model where you describe what your UI should look like for any given state — and React handles how to get there efficiently.
You write declarative components. React handles the surgical DOM updates. Everyone wins.
Next up: dive deeper into React Fiber — the engine behind React's Render phase that enables prioritised, interruptible rendering in React 18+.
Comments
Join the discussionNo comments yet. Be the first to comment.
