How React Virtual DOM works under the Hood

React never touches the Real DOM directly on every state change. Instead, it builds a lightweight JavaScript copy of the DOM (Virtual DOM), compares the new version against the old one (diffing/reconciliation), and applies only the minimal set of changes to the Real DOM. This post explains exactly how that process works.
Problem : Slow direct DOM manipulation
Before React, developers manipulated the browser's DOM directly — using document.getElementById, innerHTML, and similar APIs. This works fine for simple pages, but falls apart at scale.
Here is why : every time you touch the real DOM, the browser has to do a lot of work behind the scenes.
Reflow — recalculates the layout of all affected elements
Repaint — redraws the pixels on screen
Layout thrashing — if you read and write the DOM repeatedly in a loop, the browser recalculates layout every single time
Imagine a dynamic dashboard that updates 50 data points every second. Touching the DOM 50 times per second, even for minor changes, causes visible jank and sluggishness — because you are repeatedly forcing the browser through that expensive cycle.
The core problem is not that the DOM is badly designed. It is that updating the DOM frequently, even for small changes, is expensive. Developers needed a smarter way to batch updates and avoid unnecessary work.
Real DOM vs Virtual DOM
Understanding the Virtual DOM starts with appreciating what the real DOM actually is.
The Real DOM
The Document Object Model (DOM) is the browser's live, structured representation of your HTML. It is a tree of nodes — every element, attribute, and piece of text is a node. The browser keeps this tree in sync with what you see on screen, which is why modifying it triggers reflows and repaints.
The real DOM is powerful, but it is slow to update repeatedly because each modification can cascade through the layout engine.
The Virtual DOM
The Virtual DOM is a plain JavaScript object tree that mirrors the structure of the real DOM — but it lives entirely in memory and has no connection to the browser's rendering engine.
js
// A simplified Virtual DOM node looks like this:
{
type: 'button',
props: {
className: 'btn btn-primary',
onClick: handleClick
},
children: [
{ type: 'TEXT', value: 'Submit' }
]
}
Because it is just a JavaScript object, creating and discarding it is essentially free compared to touching the real DOM. JavaScript engines are highly optimized for object manipulation — it is several orders of magnitude faster than layout and paint operations.
| Aspect | Real DOM | Virtual DOM |
|---|---|---|
| Lives in | Browser memory (C++) | JavaScript memory (V8/SpiderMonkey) |
| Updating cost | High (reflow + repaint) | Near zero (object creation) |
| Directly renders | Yes | No — synced to real DOM selectively |
| Manipulation speed | Slow for frequent changes | Very fast |
The Initial Render: Component → Virtual DOM → Real DOM
When your React application first loads, it follows a clear three-step process.
Step 1 — JSX compiles to React elements
You write JSX, which looks like HTML inside JavaScript:
jsx
function App() {
return (
<div className="app">
<Header title="My App" />
<List items={data} />
</div>
);
}
Babel compiles this into nested React.createElement() calls:
js
React.createElement('div', { className: 'app' },
React.createElement(Header, { title: 'My App' }),
React.createElement(List, { items: data })
)
Each createElement call returns a plain object — a React element — which is the building block of the Virtual DOM tree.
Step 2 — The Virtual DOM tree is assembled
React calls your component functions top-down, collecting every React element produced. The result is a complete Virtual DOM tree representing the entire UI.
App (div.app)
├── Header (h1)
│ └── "My App"
└── List (ul)
├── Item (li) → "Item 1"
└── Item (li) → "Item 2"
Step 3 — The real DOM is built from scratch
Since there is no previous tree to compare against, React takes the Virtual DOM and creates actual DOM nodes — document.createElement, appendChild, setAttribute — for every node in the tree. This initial paint touches the DOM fully, but it only happens once.
How State or Props Changes Trigger a Re-render
Once the app is running, updates come from two sources: state changes and prop changes.
When you call a state setter from useState (in a function component), React marks that component as dirty and schedules a re-render. Similarly, if a parent component re-renders and passes different props down, the child is also re-rendered.
jsx
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 is called. React does not immediately update the DOM. Instead, it schedules a re-render, which kicks off the next two phases: creating a new Virtual DOM tree, and diffing it against the old one.
Creating a New Virtual DOM Tree
When a re-render is triggered, React calls the component function again. It executes the function from the top, with the new state values, and produces a brand-new Virtual DOM tree.
New Virtual DOM (after count = 1):
Counter (div)
├── p → "Count: 1" ← changed
└── button → "Increment"
This new tree is held in memory alongside the previous (current) tree. React now has two snapshots of the UI — one representing what is currently on screen, and one representing what should be on screen. The job of the next phase is to find the difference between them.
Reconciliation: Comparing Two Trees
Reconciliation is the process of comparing the old Virtual DOM tree against the new one to determine what, if anything, has changed in the real DOM.
Think of it like a diff tool for code — except instead of comparing lines of text, React compares tree nodes. The goal is to answer one question: what is the minimum set of real DOM mutations needed to bring the screen in sync with the new state?
Without smart heuristics, tree diffing is an O(n³) problem — comparing every node against every other node is prohibitively expensive for large UIs. React solves this with two key heuristics that make it O(n).
The Full React Lifecycle: Render → Diff → Commit
Here’s the complete React update lifecycle in simple terms:
User interacts with UI
State or props change
React creates a new Virtual DOM tree
React compares old vs new tree
Differences are identified
Minimal updates are applied
Real DOM updates
Browser then repaints the respective changed parts
Conclusion
React’s Virtual DOM is one of the key ideas that makes modern React applications efficient and scalable.
Instead of blindly updating the entire page, React intelligently:
creates Virtual DOM trees
compares changes
updates only necessary elements
This approach allows developers to build highly interactive applications while maintaining good performance. Understanding this mental model makes React’s rendering behavior much easier to reason about and debug.
Key Takeaways
The real DOM is expensive to update frequently because every change can trigger reflow and repaint.
The Virtual DOM is a plain JavaScript object tree — cheap to create, discard, and compare.
On initial render, React builds the Virtual DOM top-down and writes every node to the real DOM.
On re-render, React produces a new Virtual DOM tree and keeps the old one for comparison.
The reconciler diffs both trees using O(n) heuristics: different type → rebuild; same type → patch; lists → use keys.
The commit phase flushes only the minimal changes to the real DOM in a single synchronous pass.
This model makes React fast by doing all the heavy thinking in JavaScript and touching the DOM as little as possible.


