React
JavaScript
Virtual DOM

How Virtual DOM works in React?

July 17, 2025
20 min read

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:

  1. Create new blueprints (new Virtual DOM)
  2. Compare with current blueprints (diffing)
  3. Plan minimal changes needed (reconciliation)
  4. 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:

  1. JavaScript operations are fast - much faster than DOM operations
  2. Batching is possible - multiple changes applied at once
  3. 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:

  1. Recalculate styles - Which CSS rules apply to the changed element?
  2. Reflows the layout - Do element position/sizes need to change?
  3. Repaints the screen - What pixels need to be redrawn?
  4. 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:

  1. Initial render: Create a complete virtual DOM tree
  2. State Change: Generate a new virtual DOM tree
  3. Diffing: Compare old and new trees to identify changes
  4. 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:

  1. Compare root elements first
  2. If types match: compare attributes, recurse to children
  3. If types differ: destroy old tree, build new tree
  4. 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

  1. Stack Recursion: Deep component trees could cause stack overflow
  2. All-or-nothing: Entire tree had to be processed in one go
  3. No Prioritization: All updates had the same priority
  4. 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:

  1. Current tree: What's currently rendered on screen
  2. 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:

Fiber node inspection in browser DevTools

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.