ReactPerformanceMedium

How does the Virtual DOM work in React?

01

The Short Answer

The Virtual DOM is a lightweight JavaScript representation of the actual DOM. When state changes, React creates a new Virtual DOM tree, diffs it against the previous one (reconciliation), and applies only the minimal set of actual DOM mutations needed. This avoids the performance cost of directly manipulating the real DOM for every state change, which would be slow because DOM operations trigger layout recalculations and repaints.

02

Why Direct DOM Manipulation Is Expensive

The real DOM is a complex, heavyweight structure. Every change to it can trigger the browser's rendering pipeline — style recalculation, layout (reflow), and paint. If you update 100 elements individually, the browser might recalculate layout 100 times. This is why frameworks that directly manipulate the DOM for every data change feel sluggish with complex UIs.

  • DOM nodes are expensive objects with hundreds of properties and methods
  • Changing a DOM node can trigger style recalculation for the entire subtree
  • Layout changes (reflow) are the most expensive — they recalculate positions of all affected elements
  • Multiple individual DOM updates can cause multiple reflows in a single frame
  • Reading layout properties (offsetHeight) between writes forces synchronous reflow

The Virtual DOM solves this by batching all changes and computing the minimum set of DOM operations needed — then applying them all at once. Instead of 100 individual updates, you get one batched update that triggers a single reflow.

03

The Three-Step Process

React's Virtual DOM works in three distinct phases. Understanding these phases is key to understanding React's performance characteristics.

1

Render — Create the new Virtual DOM

When state changes, React calls your component function (and its children). Each component returns JSX, which React converts into a tree of plain JavaScript objects — the Virtual DOM. This is cheap because it's just object creation, no DOM involved.

2

Reconciliation — Diff the trees

React compares the new Virtual DOM tree with the previous one, node by node. It identifies exactly what changed: which nodes were added, removed, or updated. This diffing algorithm runs in O(n) time using heuristics.

3

Commit — Apply minimal DOM updates

React takes the list of changes from the diff and applies only those specific mutations to the real DOM. If only one text node changed in a list of 1000 items, only that one text node is updated in the real DOM.

04

What the Virtual DOM Looks Like

The Virtual DOM is just a tree of plain JavaScript objects. Each object describes a DOM node — its type, props, and children. When you write JSX, it compiles to React.createElement() calls that produce these objects. They're lightweight compared to real DOM nodes.

virtual-dom-objects.tsxtypescript
// Your JSX:
<div className="card">
  <h2>Title</h2>
  <p>Content</p>
</div>

// Compiles to:
React.createElement('div', { className: 'card' },
  React.createElement('h2', null, 'Title'),
  React.createElement('p', null, 'Content')
);

// Produces this Virtual DOM object (simplified):
{
  type: 'div',
  props: {
    className: 'card',
    children: [
      { type: 'h2', props: { children: 'Title' } },
      { type: 'p', props: { children: 'Content' } }
    ]
  }
}

// This is just a plain JS object — no DOM APIs, no browser rendering
// Creating thousands of these is fast
05

The Diffing Algorithm (Reconciliation)

A naive tree diff algorithm is O(n³) — far too slow for UI updates. React uses two heuristics to achieve O(n) diffing, which makes it practical for real-time UI updates.

Heuristic 1: Different types produce different trees

If a node changes type (e.g., <div> becomes <span>, or <Header> becomes <Footer>), React tears down the entire old subtree and builds a new one from scratch. It doesn't try to diff children of different-typed parents — the assumption is that different types produce fundamentally different structures.

type-change.tsxtypescript
// Before:
<div>
  <Counter />
</div>

// After:
<span>
  <Counter />
</span>

// React sees: div → span (different type)
// Action: destroy the entire <div> subtree (including Counter's state)
//         create a new <span> subtree from scratch
// Counter loses all its state — it's a completely new instance

Heuristic 2: Keys identify stable elements in lists

When diffing lists of children, React uses the key prop to match elements between the old and new tree. Without keys, React matches by index — which breaks when items are reordered, inserted, or removed. Keys tell React "this is the same logical element" even if its position changed.

keys-diffing.tsxtypescript
// Without keys — React matches by index (fragile)
// Before: [Alice, Bob, Charlie]
// After:  [Bob, Charlie]  (Alice removed from front)
// React thinks: index 0 changed Alice→Bob, index 1 changed Bob→Charlie, index 2 removed
// Result: updates TWO nodes and removes one (inefficient)

// With keys — React matches by identity (correct)
// Before: [key:1 Alice, key:2 Bob, key:3 Charlie]
// After:  [key:2 Bob, key:3 Charlie]  (key:1 removed)
// React thinks: key:1 removed, key:2 and key:3 unchanged
// Result: removes ONE node, moves nothing (efficient)

// That's why keys should be stable IDs, not array indices:
{todos.map(todo => (
  <TodoItem key={todo.id} todo={todo} /> // ✅ Stable identity
))}
06

A Concrete Diff Example

Let's trace through a real diff scenario. A counter component increments — only the text content of one element changes. React's diff identifies this single change and updates only that DOM text node, leaving everything else untouched.

diff-example.tsxtypescript
// Component:
function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div className="counter">
      <h2>Score</h2>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

// Virtual DOM before click (count = 0):
// { type: 'div', props: { className: 'counter', children: [
//   { type: 'h2', props: { children: 'Score' } },
//   { type: 'span', props: { children: 0 } },        ← this changes
//   { type: 'button', props: { children: '+1' } }
// ]}}

// Virtual DOM after click (count = 1):
// { type: 'div', props: { className: 'counter', children: [
//   { type: 'h2', props: { children: 'Score' } },
//   { type: 'span', props: { children: 1 } },        ← changed!
//   { type: 'button', props: { children: '+1' } }
// ]}}

// Diff result: only the text content of <span> changed (0 → 1)
// DOM operation: spanElement.textContent = '1'
// Everything else is untouched — no reflow for the entire tree
07

Common Misconceptions

🚫

The Virtual DOM is always faster than direct DOM manipulation

The Virtual DOM adds overhead (creating objects, diffing). For a single, targeted DOM update, direct manipulation is faster. The Virtual DOM wins when you have many potential changes and need to batch them efficiently — it finds the minimum set of updates automatically.
🚫

The Virtual DOM prevents all unnecessary DOM updates

If your component re-renders and produces different JSX (even slightly), React will update the DOM. The Virtual DOM doesn't prevent re-renders — it minimizes the DOM work that results from them. You still need React.memo, useMemo, etc. to prevent unnecessary re-renders.
🚫

Every framework needs a Virtual DOM for performance

Frameworks like Svelte and SolidJS achieve excellent performance without a Virtual DOM — they compile components into direct, surgical DOM updates at build time. The Virtual DOM is React's approach, not the only approach.
08

React Fiber: The Modern Implementation

React 16+ replaced the old recursive reconciler with Fiber — a reimplementation that can pause, resume, and prioritize work. Instead of diffing the entire tree synchronously (which could block the main thread for complex UIs), Fiber breaks work into small units and yields to the browser between them. This enables features like concurrent rendering, Suspense, and transitions.

Fiber's key improvement

The old reconciler was synchronous — once it started diffing, it couldn't stop until done. For large trees, this could block the main thread for 100ms+, causing dropped frames. Fiber makes reconciliation interruptible, so high-priority updates (like user input) can jump the queue ahead of low-priority updates (like off-screen rendering).

09

Why Interviewers Ask This

The Virtual DOM is React's core architectural decision. Interviewers ask this to check whether you understand why React doesn't directly manipulate the DOM, how the diffing algorithm achieves O(n) performance, why keys matter for list reconciliation, and the tradeoffs of this approach vs alternatives. It shows you understand React at an architectural level — not just how to use it, but why it works the way it does.

Quick Revision Cheat Sheet

What it is: Lightweight JS object tree representing the UI structure

Process: Render (create VDOM) → Reconcile (diff) → Commit (minimal DOM updates)

Diffing: O(n) using two heuristics: type changes = rebuild, keys = identity

Keys: Tell React which list items are the same between renders

Not always faster: Adds overhead — wins for complex UIs with many potential changes

Fiber: Modern implementation — interruptible reconciliation, priority scheduling