React's Virtual DOM : Behind the Scenes

If you've worked with React even for a little while, you've probably heard about the Virtual DOM. People often say it's one of the main reasons React feels fast and efficient. But what does it actually mean? Why was it introduced? And what really happens behind the scenes when a component updates?
In this article, we'll break everything down step by step, starting from the problem the Virtual DOM was designed to solve, all the way to how React updates only the necessary parts of the UI to keep your app smooth and performant.
The Problem: Why Direct DOM Manipulation Is Slow
To understand why the Virtual DOM exists, you first need to understand the problem it solves.
The browser's Real DOM, or Document Object Model, is a tree-like structure of your HTML elements. Whenever something changes on the page, like updating text, adding an element, or changing a class name, the browser has to perform several tasks:
Recalculate styles
Reflow the layout to reposition elements
Repaint the updated content on the screen
These operations are expensive, especially when updates happen frequently.
For small websites, this is usually fine. But modern web apps constantly update their UI. Users type into inputs, lists refresh, counters change, and components re-render. If every change directly updates the Real DOM, the browser ends up doing a lot of unnecessary work.
This was a common problem in early jQuery applications. Developers manually updated DOM elements, but as apps became more complex, keeping the UI in sync became difficult and error-prone.
React introduced the Virtual DOM to make this update process faster and more efficient.
Real DOM vs Virtual DOM
Before understanding how React works internally, it's important to know the difference between the Real DOM and the Virtual DOM.
The Real DOM is the actual tree of HTML elements that the browser renders on the screen. It is heavy and expensive to update because every change can trigger layout calculations and re-rendering.
The Virtual DOM is a lightweight JavaScript object that mirrors the Real DOM. It is just data, so creating and updating it is much faster.
Think of the Real DOM as the actual house, and the Virtual DOM as its blueprint. Changing a blueprint is quick and easy, while modifying the real house takes more effort.
React first updates the Virtual DOM, finds the minimum changes required, and then updates only those parts in the Real DOM.
Here's a simple mental model of what a Virtual DOM node looks like:
// JSX you write:
<div className="card">
<h2>Hello, World</h2>
<p>Welcome to React</p>
</div>
// Gets turned into a Virtual DOM object like:
{
type: 'div',
props: { className: 'card' },
children: [
{
type: 'h2',
props: {},
children: ['Hello, World']
},
{
type: 'p',
props: {},
children: ['Welcome to React']
}
]
}
This object lives entirely in memory. React can create, compare, and throw away these objects at will, all without touching the browser.
The Initial Render Process
When your React application loads for the first time, this is what happens step by step:
Step 1 - React reads your components
You create components as functions or classes that return JSX. React calls these components and collects their JSX output.
Step 2 - JSX gets converted into JavaScript
JSX is not understood directly by the browser. Babel compiles it into React.createElement() calls, which create Virtual DOM objects.
Step 3 - React creates the Virtual DOM tree
Starting from the root component, usually <App />, React recursively processes all components and builds one complete Virtual DOM tree.
Step 4 - React updates the Real DOM
React converts the Virtual DOM into actual DOM elements and inserts them into the browser. This is the only time React builds the entire UI from scratch. Future updates only change the necessary parts instead of rebuilding everythin g.
Component Functions
↓
JSX → React.createElement()
↓
Virtual DOM Tree (in memory)
↓
Real DOM (browser renders it)
How State or Props Changes Trigger a Re-render
Once the app is running, a state or prop change can trigger a re-render. For example, a user clicks a button that calls setState, or a parent component passes new props.
React marks that component as needing an update. During the next render cycle, React calls the component again with the updated state or props. The component returns new JSX, and React creates a new Virtual DOM tree from it.
An important thing to understand is that React does not update the Real DOM immediately. It first creates the updated Virtual DOM completely in memory, then compares it with the previous version.
This is what makes React efficient. Most of the work happens on lightweight JavaScript objects, while the Real DOM is updated only where changes are actually needed.
Creating a New Virtual DOM Tree
Every time a component re-renders, React creates a new Virtual DOM subtree for that component and its children. This new tree represents how the UI should look based on the latest state and props.
Consider a simple counter:
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
When count is 0, the Virtual DOM tree looks like:
{
type: 'div',
children: [
{ type: 'p', children: ['Count: 0'] },
{ type: 'button', children: ['Increment'] }
]
}
After the button is clicked, count becomes 1, and React creates a new Virtual DOM tree:
{
type: 'div',
children: [
{ type: 'p', children: ['Count: 1'] }, // ← changed
{ type: 'button', children: ['Increment'] } // ← same
]
}
At this point, React has two Virtual DOM trees, the old tree from the previous render and the new tree from the current render. React then compares both trees to find what has changed.
Diffing and Reconciliation
Reconciliation is the process React uses to compare the old Virtual DOM tree with the new one to detect changes. This comparison process is called diffing.
Comparing two large trees can be very expensive, but React uses smart rules to make it fast and efficient.
Rule 1 - Different element types create different trees
If a <div> changes to a <section>, React removes the old subtree and creates a new one instead of comparing them deeply.
Rule 2 - Keys help React track list items
When rendering lists, React uses the key prop to identify items between renders. Without keys, React assumes items in the same position are the same, which can cause incorrect updates.
This is why React warns you about missing keys in lists. Keys help the diffing algorithm work correctly and efficiently.
How React Finds the Minimal Required Changes
During diffing, React compares the old and new Virtual DOM trees node by node.
At each step, React checks:
Is the element type the same?
If a<p>is still a<p>, React keeps the existing DOM node. If the type changes, React removes the old node and creates a new one.Did the props change?
If properties likeclassName,style, oronClickchanged, React updates only those specific attributes.Did the children change?
React recursively compares child elements and applies the same process.For lists, do the keys match?
React useskeyprops to identify which items were added, removed, or moved.
After diffing, React creates a minimal list of updates needed for the Real DOM. For example, in a counter app, React may only update the text from "Count: 0" to "Count: 1" while leaving the button unchanged.
Updating Only the Changed Nodes in the Real DOM
Once diffing is finished, React enters the commit phase. This is the stage where React finally updates the browser's Real DOM.
React only applies the changes identified during diffing, such as:
Updating text content
Changing an attribute
Adding a new DOM element
Removing an old element
Since React already calculated everything using the Virtual DOM, the actual DOM updates are minimal and efficient. This reduces expensive browser operations like reflow and repaint.
In a counter example, React only updates the text inside the <p> tag from "Count: 0" to "Count: 1". The <button> and the rest of the page remain unchanged.
Why This Approach Improves Performance
Now let's understand why this architecture works so well.
Batching updates
React can combine multiple state updates into a single render cycle. Instead of updating the DOM multiple times, React creates one optimized update.
Avoiding unnecessary work
React compares the old and new Virtual DOM trees first. If nothing changed, it skips updating the Real DOM completely.
Reducing expensive browser operations
Traditional JavaScript often mixes DOM reads and writes, which can force repeated layout recalculations. React avoids this by applying DOM updates together during the commit phase.
Predictable UI updates
Since React manages DOM updates itself, the UI stays in sync with the application state without manual DOM handling.
The Virtual DOM is not magic, it also has some overhead because React needs to create and compare JavaScript objects. But in most real-world applications, reducing expensive Real DOM operations gives a much bigger performance benefit.
The Full React Update Lifecycle: Render → Diff → Commit
Let's tie everything together into one clear flow.
┌──────────────────────────────────┐
│ TRIGGER │
│ State / Props / forceUpdate │
└────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────┐
│ RENDER PHASE │
│ React re-renders components │
│ Creates new Virtual DOM tree │
│ No Real DOM updates yet │
└────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────┐
│ DIFF PHASE │
│ Compare old vs new VDOM │
│ Detect changed nodes │
│ Create minimal patch list │
└────────────────┬─────────────────┘
│
▼
┌──────────────────────────────────┐
│ COMMIT PHASE │
│ Update changed Real DOM nodes │
│ Browser repaints necessary UI │
│ useEffect runs after commit │
└──────────────────────────────────┘
The React update process is divided into three main phases:
Render phase - React calculates what the UI should look like based on the latest state and props. This work happens only in memory using the Virtual DOM.
Diff phase - React compares the new Virtual DOM tree with the previous one to find what changed. This is where reconciliation happens.
Commit phase - React updates the Real DOM with only the required changes. Effects like useEffect also run after this phase.
Conclusion
The Virtual DOM is one of React's core concepts, but its main purpose is simple, keeping the UI updated efficiently without developers manually handling DOM changes.
Here’s the mental model to remember:
The Real DOM is expensive to update
The Virtual DOM is a lightweight in-memory copy
React compares the old and new Virtual DOM trees through diffing
Only the required changes are applied to the Real DOM
This render → diff → commit cycle keeps running as users interact with your app, state changes, or new data arrives. React handles the complex DOM updates internally, so developers only need to describe how the UI should look for a given state.
Understanding this flow also helps you use optimization tools like React.memo, useMemo, useCallback, and key props more effectively, because all of them help React reduce unnecessary work during diffing.



