Skip to main content

Command Palette

Search for a command to run...

How React Virtual DOM Works Under the Hood

Updated
11 min read
How React Virtual DOM Works Under the Hood
T
Software Engineer exploring full-stack systems, scalable architectures, and product-focused development. Experienced with React, React Native, Flutter, TypeScript, Node.js, monorepos, and tRPC. Passionate about building performant applications, solving complex engineering problems, and continuously learning across domains.

Modern applications constantly update the UI.

  • Typing inside an input field.

  • Receiving notifications.

  • Updating chat messages.

  • Rendering huge lists.

  • Changing component state.

  • Switching routes.

If every small UI change directly manipulated the browser DOM repeatedly, applications would become slow very quickly.

This is because the Real DOM is expensive to update.

Every DOM mutation can potentially trigger:

  • layout recalculation

  • style recomputation

  • repainting

  • reflow

  • compositing work

And as applications become larger, these operations become increasingly expensive.

React’s Virtual DOM architecture was designed to solve this problem.

But contrary to popular belief, React is not simply:

"comparing two DOM trees"

Internally, React works with:

  • React Elements

  • Virtual DOM trees

  • reconciliation

  • Stack Reconciler

  • Fiber architecture

  • scheduling

  • render and commit phases

  • incremental rendering

Understanding these concepts completely changes how you think about React rendering.


The Real Problem React Was Trying to Solve

Before React existed, frontend applications often directly manipulated the browser DOM manually.

Example:

document.getElementById("title").innerText = "Hello";

or:

element.style.width = "500px";

This works fine for small applications.

But modern applications constantly update UI:

  • forms

  • chats

  • notifications

  • animations

  • dashboards

  • live feeds

And every DOM update can potentially trigger expensive browser operations.

The browser may need to:

  • recalculate layouts

  • recompute styles

  • repaint pixels

  • rebuild compositing layers

The bigger the UI becomes, the more expensive repeated DOM mutations become.

React’s first big idea was:

"Instead of mutating the DOM repeatedly, first calculate changes efficiently in memory."

That idea eventually became:

  • Virtual DOM

  • reconciliation

But the important part many developers miss is this:

Virtual DOM alone was NOT the full solution.

React also needed:

  • efficient tree traversal

  • intelligent diffing

  • update scheduling

  • rendering coordination

The original system responsible for this was Stack Reconciler


What Virtual DOM Actually Is

One of the most common misconceptions is:

"Virtual DOM is a copy of the Real DOM."

That explanation is incomplete.

Virtual DOM is actually a lightweight JavaScript representation of UI.

Example:

function App() {
  return <h1>Hello</h1>;
}

React conceptually converts this into:

{
  type: "h1",
  props: {
    children: "Hello"
  }
}

This is NOT browser DOM.

It is simply a JavaScript object describing UI structure.


React Elements: The Foundation of Virtual DOM

When JSX is written:

<h1>Hello</h1>

React internally creates React Elements.

React Elements are immutable JavaScript objects describing:

  • component type

  • props

  • children

Conceptually:

{
  type: "h1",
  props: {
    children: "Hello"
  }
}

These React Elements later become part of the Virtual DOM tree.

Important distinction:

  • JSX is syntax

  • React Elements are objects

  • Virtual DOM is tree structure built from these objects


Initial Render Process

Suppose:

function App() {
  return (
    <div>
      <h1>Hello</h1>
      <button>Click</button>
    </div>
  );
}

React creates a Virtual DOM tree:

App
 └── div
      ├── h1
      └── button

Then React creates actual browser DOM nodes:

<div>
  <h1>Hello</h1>
  <button>Click</button>
</div>

This first rendering phase is called Mounting

The flow becomes:


Stack Reconciler: React’s Original Rendering Engine

Before Fiber existed, React internally used something called the Stack Reconciler

This architecture is extremely important because Fiber was essentially React rewriting this entire system.

It was called “stack” reconciler because React relied heavily on the JavaScript call stack during recursive traversal.

Suppose we had:

<App>
  <Header />
  <Main />
  <Footer />
</App>

React converted this into a component tree:

App
 ├── Header
 ├── Main
 └── Footer

Whenever state changed:

setState(...)

React immediately started recursively traversing the component tree.

Conceptually:

render(App)
 └── render(Header)
 └── render(Main)
      └── render(Content)

This traversal was:

  • synchronous

  • recursive

  • depth-first

and heavily depended on the JavaScript call stack itself.

Why It Was Called “Stack” Reconciler?

Every nested component render created additional frames inside the JavaScript call stack.

Example:

render(App)
 └── render(Main)
      └── render(Content)
           └── render(Comment)

Each recursive render pushed more execution onto the stack.

React itself did not fully control execution.

The JavaScript engine controlled it through recursion.

This later became one of the biggest architectural limitations.

Stack Reconciler Used Depth-First Traversal

Very important concept.

Stack Reconciler traversed the component tree using Depth-First Search (DFS)

Example tree:

A
├── B
│   ├── D
│   └── E
└── C

Traversal order:

A → B → D → E → C

React fully finished children before siblings.

This traversal happened synchronously.

The Biggest Problem with Stack Reconciler

The biggest issue was:

Once rendering started, React could NOT pause work.

This became dangerous for large applications.

Suppose 10,000 components re-rendering,

Old React behaved roughly like "render everything NOW"

During this time:

  • browser could not paint

  • animations froze

  • scrolling lagged

  • typing became janky

because JavaScript execution blocked the main thread.

Why Browser Freezes Happened?

JavaScript in browsers is single-threaded.

The browser event loop roughly works like:

Call Stack
↓
Microtasks
↓
Rendering/Painting
↓
Macrotasks

If React occupied the JavaScript call stack too long browser cannot paint frames

And smooth 60 FPS rendering roughly needs 16ms per frame

Large synchronous reconciliation could easily exceed this budget.

This was one of the biggest reasons Fiber architecture was eventually created.


How React Handles Updates

Suppose:

function Counter() {

  const [count, setCount] = useState(0);

  return <h1>{count}</h1>;

}

When:

setCount(1)

is called, React does NOT immediately mutate the browser DOM.

Instead React starts an internal update process.

Step 1 — New React Elements Are Created

Old representation:

<h1>0</h1>

New representation:

<h1>1</h1>

React creates a completely new Virtual DOM representation.

Step 2 — Reconciliation Starts

React compares old tree with new tree

This process is called Reconciliation

The comparison algorithm is commonly called Diffing

On a side note React does NOT compare DOM vs DOM

React compares Old React Tree vs New React Tree

Then calculates DOM mutations afterward.

This distinction is extremely important.


What Exactly is React’s Diffing Algorithm?

Previously, I said "React compares old Virtual DOM with new Virtual DOM."

But internally, this problem is actually much deeper from a computer science perspective.

Because what React is really solving is Tree Difference Problem

And mathematically, this is an extremely expensive problem.

Suppose we have two UI trees.

Old tree:

A
├── B
└── C

New tree:

A
├── B
└── D

React must determine:

"What is the minimum set of operations required to transform old tree into new tree?"

Possible operations include:

  • insert node

  • delete node

  • move node

  • replace node

  • update properties

This is conceptually similar to:

  • string edit distance

  • graph transformation problems

  • tree edit distance algorithms

For arbitrary trees, optimal tree comparison algorithms become very costly.

General tree diffing complexity is approximately:

O(n^3)

This is extremely important.

Suppose there are 1000 nodes

Then for O(n^3):

1000^3 = 1,000,000,000

That means potentially, one billion comparisons/operations for large trees.

Completely impractical for real-time UI rendering.

True tree diffing is so expensive because a mathematically optimal algorithm may need to compare:

every subtree with many other possible subtrees

Example:

Old tree

A
├── B
│   ├── D
│   └── E
└── C

New tree:

A
├── C
└── B
    ├── D
    └── E

Now algorithm must determine:

  • was B moved?

  • was C moved?

  • should subtree be reused?

  • should nodes be deleted and recreated?

Many possible transformation combinations exist.

This creates combinatorial explosion.

React intentionally sacrifices perfect mathematical optimality

in exchange for extremely fast practical performance

Instead of solving the general tree-edit-distance problem perfectly,

React uses Heuristics


React’s Heuristics

1.) Different Types Produce Different Trees

Example:

<div />

becomes:

<span />

React immediately assumes Entire subtree changed

No deep comparison happens.

This dramatically reduces computation.

2.) Elements of Same Type Can Be Reused

Example:

<h1 className="red">Hello</h1>

becomes:

<h1 className="blue">Hello</h1>

React reuses same <h1> node.

Only changed properties update.

3.) Using Keys

Without keys:

Old:

A B C

New:

X A B C

Naive sequential comparison becomes:

A → X
B → A
C → B
Insert C

This creates many unnecessary mutations.

With keys:

<li key="a">A</li>
<li key="b">B</li>

React can internally build lookup structures conceptually similar to:

Map<key, oldNode>

Now React can perform near O(1) lookups for children identity.

Instead of:

  • repeatedly scanning children

  • recursively guessing identity

React immediately understands:

  • which nodes stayed

  • which moved

  • which were inserted

This massively improves reconciliation efficiency.

This is one of React’s biggest innovations.

General tree diff:

O(n^3)

React heuristic diff:

O(n)

That reduction is enormous.

React Achieves Near O(n) time complexity

Because React avoids:

  • exhaustive subtree comparison

  • arbitrary move detection

  • mathematically optimal matching

Instead React performs:

  • mostly linear traversal

  • heuristic assumptions

  • keyed lookups

  • shallow sequential comparison

This transforms reconciliation from theoretically optimal but impractical

into slightly imperfect but extremely fast

which is exactly what real-time UI systems need.

The update flow roughly becomes:


Enter React Fiber

Eventually React team realized Stack Reconciler architecture could not scale well for increasingly complex applications.

So React 16 introduced Fiber Architecture

Fiber was not a small improvement.

It was a complete rewrite of React’s rendering engine.

The primary goal:

Make rendering interruptible.

Fiber allowed React to:

  • pause work

  • resume later

  • prioritize updates

  • schedule rendering intelligently

This fundamentally changed React architecture.

Fiber Replaced Recursive Traversal

This was one of the biggest architectural shifts.

Old Stack Reconciler depended on recursive call stack traversal.

Fiber replaced this with manually controlled traversal using linked Fiber nodes.

Each Fiber stores references like:

  • child

  • sibling

  • return(parent)

This allows React to manually control rendering flow instead of JavaScript recursion controlling it.

Now React can:

  • pause rendering

  • continue later

  • prioritize work

  • interrupt low-priority rendering

Impossible with old Stack Reconciler.

Fiber internally maintains TWO trees.

1.) Current Tree

Represents UI currently visible on screen.

2.)Work-In-Progress Tree

New tree being prepared in memory.

React builds updates here first.

Once rendering finishes, swap trees

This concept is called Double Buffering

One of the most important ideas in Fiber architecture.

Now, let's dive into the phases of work in Fiber

1.) Render Phase

React:

  • builds new Fiber tree

  • performs reconciliation

  • calculates changes

  • prepares effects

This phase:

  • can pause

  • can resume

  • can be interrupted

No DOM mutations happen here.

2.) Commit Phase

Once work is finalized:

  • React updates actual DOM

This phase is synchronous and cannot be interrupted.

Flow:


Incremental Rendering:

Fiber breaks rendering into small units of work.

Instead of:

BIG TASK

React performs:

small task
small task
small task

Between tasks React checks:

Should browser get control now?

This enables:

  • smoother animations

  • responsive typing

  • non-blocking rendering

This was impossible with old Stack Reconciler.


Scheduling and Priorities:

Fiber introduced priorities.

High priority:

  • typing

  • button clicks

  • animations

Low priority:

  • hidden content

  • background rendering

  • non-visible updates

React can now delay less important work to keep applications responsive.

This is one of the biggest reasons modern React feels smoother.


What Actually Gets Updated in the DOM:

Suppose:

<div>
  <h1>{count}</h1>
  <BigList />
</div>

If count changes:

React may only update:

<h1>

while skipping BigList entirely if unchanged.

This selective updating is one of React’s biggest optimizations.


Why Virtual DOM Improves Performance:

One important clarification:

Virtual DOM itself is NOT magically faster than Real DOM.

The real advantage comes from:

  • intelligent reconciliation

  • Stack Reconciler initially

  • Fiber scheduling later

  • batching

  • prioritization

  • incremental rendering

  • minimal DOM mutations

That is the real innovation.


Final Mental Model

Modern React is far more than a library that “updates the DOM efficiently.”

Internally React behaves more like:

  • rendering engine

  • scheduler

  • UI coordinator

The Virtual DOM is only one piece of a much larger architecture involving:

  • reconciliation

  • Stack Reconciler

  • Fiber trees

  • scheduling

  • priorities

  • incremental rendering

  • intelligent DOM mutations

And understanding this internal mental model fundamentally changes how you think about:

  • rendering

  • re-renders

  • memoization

  • keys

  • performance optimization

  • React architecture itself.

More from this blog