How Virtual DOM works in React?
React Virtual DOM
When I was learning React I was introduced to the concept of the virtual DOM. One of the most remarkable features of React is its ability to efficiently update the DOM when the state of a component changes. Most guides and tutorials explain how the virtual DOM works, but I wanted to dive deeper into the details. I'd like to share my understanding of how the virtual DOM works in React and how it enables efficient updates to the DOM.
What is the Virtual DOM?
From React's official documentation:
The virtual DOM (VDOM) is a programming concept where an ideal, or “virtual”, representation of a UI is kept in memory and synced with the “real” DOM by a library such as ReactDOM. This process is called reconciliation.
When you want to renovate a room, you don't immediately start tearing down walls. Instead, you:
- Create new blueprints (new Virtual DOM)
- Compare with current blueprints (diffing)
- Plan minimal changes needed (reconciliation)
- Execute only necessary construction (DOM updates)
What does virtual mean?
The term "virtual" means it only exists in memory as JavaScript objects. Some benefits of this approach:
- JavaScript operations are fast - much faster than DOM operations
- Batching is possible - multiple changes applied at once
- Calculations are cheap - compute before rendering
The Problem: Why DOM Updates Are Expensive
Before we dive into how Virtual DOM solves problems, we need to understand why DOM updates are expensive in the first place.
The Cost of DOM Operations
When you manipulate the DOM, the browser has to do a lot of work:
// This simple change triggers multiple expensive operations
document.getElementById('title').textContent = 'New Title';
Behind scenes, the browser has to:
- Recalculate styles - Which CSS rules apply to the changed element?
- Reflows the layout - Do element position/sizes need to change?
- Repaints the screen - What pixels need to be redrawn?
- Composite layers - How do overlapping elements combine?
The "Batching" Problem
Consider the following example:
// Each line triggers a separate reflow/repaint cycle
element.style.width = '100px'; // Reflow + Repaint
element.style.height = '200px'; // Reflow + Repaint
element.style.color = 'blue'; // Repaint
// 3 separate render cycles instead of 1
In this case, the browser has to recalculate styles, reflow the layout, repaint the screen, and composite layers for each change.
Real DOM vs Virtual DOM comparison
Traditional DOM Manipulation
// Direct DOM manipulation (expensive)
const container = document.createElement('div');
container.className = 'container';
const heading = document.createElement('h1');
heading.textContent = 'Hello World';
const button = document.createElement('button');
button.textContent = 'Click me';
button.addEventListener('click', handleClick);
container.appendChild(heading);
container.appendChild(button);
document.body.appendChild(container);
// To update the heading:
heading.textContent = 'Hello React!'; // Direct DOM manipulation
Virtual DOM approach
// Virtual DOM (just JavaScript objects)
const createVirtualDOM = (title) => ({
type: 'div',
props: {
className: 'container',
children: [
{
type: 'h1',
props: { children: title }
},
{
type: 'button',
props: {
onClick: handleClick,
children: 'Click me'
}
}
]
}
});
// Initial render
let currentVDOM = createVirtualDOM('Hello World');
// After state change
let newVDOM = createVirtualDOM('Hello React!');
// React compares currentVDOM vs newVDOM
// Only updates the specific text node that changed
Behind the scenes: the algorithm
The Virtual DOM is fundamentally a tree diffing algorithm that operates on JavaScript objects representing DOM elements. Here are the algorithm's steps:
- Initial render: Create a complete virtual DOM tree
- State Change: Generate a new virtual DOM tree
- Diffing: Compare old and new trees to identify changes
- Reconciliation: Apply minimal changes to the real DOM
The Diffing Algorithm
React uses heuristic assumptions to make diffing O(n) instead of O(n^3):
Assumption 1: Elements of different types produce different trees
- If
div
becomes<span>
, React destroys the entire subtree
Assumption 2: Developers should provide key
prop to elements that are likely to change
- Without keys, React falls back to positional matching
Tree Traversal Strategy
React uses depth-first traversal with these steps:
- Compare root elements first
- If types match: compare attributes, recurse to children
- If types differ: destroy old tree, build new tree
- For lists: use keys to optimize insertion/deletion/reordering
Let's see how this works in practice.
// Old tree
<div>
<h1>Title</h1>
<ul>
<li key="1">Item 1</li>
<li key="2">Item 2</li>
</ul>
</div>
// New tree
<div>
<h1>New Title</h1>
<ul>
<li key="0">New Item</li>
<li key="1">Item 1</li>
<li key="2">Item 2</li>
</ul>
</div>
// React's diffing process:
// 1. div → div: same type, continue
// 2. h1 → h1: same type, text changed → update text content
// 3. ul → ul: same type, continue
// 4. Compare children using keys:
// - key="0": new item → insert
// - key="1": existing → no change
// - key="2": existing → no change
// Result: Only 2 DOM operations (text update + element insertion)
Limitations of the Original Implementation
The original Virtual DOM approach (React ≤15) had significant limitations:
The Syncronous Blocking Problem
// In React 15 and earlier:
function heavyComponent() {
return (
<div>
{/* Imagine 1000s of elements here */}
{massiveArray.map(item => <ComplexItem key={item.id} data={item} />)}
</div>
);
}
// Problem: Once reconciliation started, it couldn't be interrupted
// Main thread was blocked until entire tree was processed
// Result: Janky animations, unresponsive UI
Performance Issues
- Stack Recursion: Deep component trees could cause stack overflow
- All-or-nothing: Entire tree had to be processed in one go
- No Prioritization: All updates had the same priority
- Poor Animation Performance: Long renders blocked the main thread
This led to a fundamental rewrite...
React Fiber: The Evolution
What is React Fiber?
From official Github repo:
React Fiber is an ongoing reimplementation of React's core algorithm. It is the culmination of over two years of research by the React team.
Fiber is a ground-up rewrite of the reconciler that solves the blocking problem by introducing interruptible rendering.
Why Fiber Was Revolutionary
Before Fiber (React 15):
// Synchronous, blocking reconciliation
function reconcile(element) {
// Process entire tree - can't stop until done
processComponent(element);
element.children.forEach(child => reconcile(child)); // Blocks main thread
}
With Fiber (React 16):
// Interruptible, priority-based reconciliation
function workLoop(deadline) {
while (nextUnitOfWork && deadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // Can be paused!
}
if (nextUnitOfWork) {
// More work to do, schedule continuation
requestIdleCallback(workLoop);
}
}
Fiber Architecture
A Fiber node is a JavaScript object representing a "unit of work" with the following properties:
// Simplified Fiber node structure
const fiberNode = {
// Identity
tag: 0, // Component type (function, class, host element)
key: null, // React key
elementType: 'div', // The element type
type: 'div', // Resolved type
// Relationships
child: null, // First child fiber
sibling: null, // Next sibling fiber
return: null, // Parent fiber
// State
memoizedState: null, // Component state
memoizedProps: null, // Last rendered props
pendingProps: null, // New props
// Work
flags: 0, // Side effects
subtreeFlags: 0, // Subtree side effects
alternate: null, // Alternate fiber (double buffering)
// DOM
stateNode: null, // Reference to DOM node or component instance
};
Fiber Tree Structure
Unlike a traditional tree, Fiber creates a linked list structure:
App
|
Header ──→ Main ──→ Footer
| |
Logo ──→ Nav Content
Each node has:
- Child: Points to first child
- Sibling: Points to next sibling
- Return: Points to parent
Double Buffering
Fiber uses two trees simultaneously:
- Current tree: What's currently rendered on screen
- Work-in-progress tree: Being built for the next render
// Current fiber points to work-in-progress
currentFiber.alternate = workInProgressFiber;
// Work-in-progress points back to current
workInProgressFiber.alternate = currentFiber;
// After render completes, work-in-progress becomes current
This enables React to:
- Build the new tree without affecting current UI
- Abandon work if higher priority updates come in
- Compare efficiently between current and work-in-progress
Let's see VDOM in Action
Inspecting Fiber Nodes in Browser
Open Chrome DevTools on any React app. Find an element that might be a React component, like navbar. Right-click and select "Inspect Element". You'll notice element is selected in the Elements panel. Then, in the console, run:
// Select any React element and run in console:
const element = $0; // Currently selected element
// Find the fiber node
const fiberKey = Object.keys(element).find(key =>
key.startsWith('__reactFiber')
);
const fiber = element[fiberKey];
console.log('Fiber node:', fiber);
console.log('Component type:', fiber.type);
console.log('Props:', fiber.memoizedProps);
console.log('State:', fiber.memoizedState);
// Navigate the fiber tree
console.log('Parent:', fiber.return);
console.log('First child:', fiber.child);
console.log('Next sibling:', fiber.sibling);
You'll see the following output:
Conclusion
Virtual DOM shines when we're dealing with complex UIs where multiple components need updates, when we want cross-browser consistency, or when we want the declarative programming model that lets us describe what the UI should look like rather than imperatively manipulating it step by step. The real performance wins come from avoiding unnecessary renders through proper key usage.
What I've learned from digging into Fiber's internals is that Virtual DOM represents one of React's most ingenious solutions to web development challenges—it's a strategy, not magic, that transforms expensive DOM manipulation problems into manageable JavaScript operations. Fiber's interruptible rendering was revolutionary because it solved the blocking problem that plagued React 15 and earlier, where long renders would freeze your entire UI.