How React Virtual DOM Works Under the Hood

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.




