Skip to main content

Command Palette

Search for a command to run...

How React's Virtual DOM Works Under the Hood

Published
23 min read
How React's Virtual DOM Works Under the Hood
S
I write code , that run in the browser and someone else's machine. And sometimes I also write articles

A Deep Dive into One of React's Most Powerful Concepts


Introduction

If you have spent any time learning React, you have almost certainly heard the term Virtual DOM mentioned as one of the reasons React is fast and efficient. But what does it actually mean? Why does it exist? And what is really happening behind the scenes when you update a piece of state or pass new props to a component?

This article is designed to give you a thorough, well-structured understanding of the Virtual DOM — not just a surface-level definition, but a genuine mental model of the entire process from the moment React renders your component for the first time, through to how it decides what to update in the browser when something changes. By the end of this piece, you should be able to explain the Virtual DOM confidently and understand why it represents such a significant improvement over direct, uncontrolled DOM manipulation.

We will cover this topic step by step, building up the full picture layer by layer.


Section 1 — The Problem: Why Direct DOM Manipulation Is Expensive

Before we can appreciate what the Virtual DOM does for us, we need to understand the problem it was designed to solve. That problem is the cost of interacting directly with the browser's Real DOM.

What Is the Real DOM?

The Real DOM (Document Object Model) is the browser's live, structured representation of your HTML page. When the browser loads your HTML, it parses it and constructs a tree of objects — every div, h1, button, span, and p becomes a node in this tree. This is the Real DOM. JavaScript can then interact with this tree to read values, change text, add elements, remove elements, and respond to user events.

At first glance, this sounds perfectly reasonable. And for simple static pages or occasional updates, direct DOM manipulation works fine. The trouble begins when you are building a complex, data-driven, interactive application — the kind of application React was built for.

Why Is the Real DOM Slow to Update Frequently?

The Real DOM is slow to update frequently for a number of interconnected reasons, and understanding these reasons is key.

Reflow and Repaint are the two most expensive processes in the browser rendering pipeline. When you make a change to the DOM — say you update the text inside a <div> or add a new element — the browser often needs to recalculate the layout of the entire page (this is called reflow or layout) and then visually repaint the affected area on screen (this is called repaint). These operations are computationally expensive because the browser must work out how every element relates to every other element in terms of size and position before it can draw pixels to the screen.

Now consider a modern web application like a Twitter feed, a live search results page, or a shopping cart. Data changes constantly — new items appear, quantities update, prices change, filters are applied. If every single data change caused a direct write to the Real DOM, and each write triggered a reflow and repaint cycle, the application would become sluggish very quickly, especially on lower-powered devices.

Imperative DOM manipulation also introduces another layer of complexity. Without a framework, you as the developer are responsible for manually tracking which part of the page needs to change, selecting the right DOM node, and applying the update. In a large application, this becomes extremely difficult to manage correctly and efficiently. Developers often ended up writing code like:

// Manually finding and updating DOM nodes — messy, fragile, and slow at scale
const cartCount = document.getElementById('cart-count');
cartCount.innerText = newCount;

const priceDisplay = document.querySelector('.total-price');
priceDisplay.innerText = `$${newTotal}`;

This approach works for trivial cases, but it does not scale. It leads to bugs, inconsistencies between the UI and the underlying data, and performance problems when too many updates happen in rapid succession.

This is precisely the problem React set out to solve, and the Virtual DOM is its primary mechanism for doing so.


Section 2 — Introducing the Virtual DOM: A Lightweight JS Representation

React's solution to the problem of expensive Real DOM manipulation is elegant in its design. Rather than letting your code interact directly with the Real DOM every time something changes, React introduces an intermediary layer — the Virtual DOM.

What Is the Virtual DOM?

The Virtual DOM is a lightweight, in-memory JavaScript representation of the Real DOM. It is not a browser API or a special technology — it is simply a plain JavaScript object tree that mirrors the structure of your UI.

When React renders your component tree, it does not immediately go and create or update Real DOM nodes. Instead, it first constructs this JavaScript object representation — the Virtual DOM tree — which describes what the UI should look like. Each node in this tree is a plain JavaScript object, sometimes called a React Element, that describes a single piece of the UI.

A React Element might look conceptually like this under the hood:

// This is a simplified illustration of what a React Element (Virtual DOM node) looks like
{
  type: 'div',
  props: {
    className: 'card',
    children: [
      {
        type: 'h2',
        props: {
          children: 'Product Title'
        }
      },
      {
        type: 'p',
        props: {
          children: 'Price: $29.99'
        }
      }
    ]
  }
}

This is a plain JavaScript object. Creating and comparing JavaScript objects is orders of magnitude faster than creating and comparing Real DOM nodes, because JavaScript objects live purely in memory and require no browser rendering machinery to process. There are no layout calculations, no reflow, no repaint — just data manipulation in memory.

JSX and React Elements

It is worth noting at this point that when you write JSX in your React component:

function ProductCard() {
  return (
    <div className="card">
      <h2>Product Title</h2>
      <p>Price: $29.99</p>
    </div>
  );
}

This JSX is not HTML. It is syntactic sugar that gets compiled by tools like Babel into calls to React.createElement(), which in turn produces those plain JavaScript objects (React Elements) that form the Virtual DOM tree. You are describing what you want the UI to look like, and React handles the process of making the Real DOM match that description.


Section 3 — The Initial Render Process

Now that we understand what the Virtual DOM is, let us walk through what happens the very first time a React application renders to the screen. This is called the initial render or mount.

Step 1: React Builds the Virtual DOM Tree

When your React application first starts, React calls your root component function (or class render method) and traverses the entire component tree — calling each component in turn, collecting the React Elements they return, and assembling them into a complete Virtual DOM tree that represents your entire UI.

Consider a simple example:

function App() {
  return (
    <div className="app">
      <Header />
      <ProductList />
    </div>
  );
}

function Header() {
  return <h1>My Shop</h1>;
}

function ProductList() {
  return (
    <ul>
      <li>Item One</li>
      <li>Item Two</li>
    </ul>
  );
}

React will call App, which returns a Virtual DOM node for a div containing Header and ProductList. React then calls Header, which returns a Virtual DOM node for an h1. React calls ProductList, which returns a ul with two li children. All of these are assembled into a single, complete Virtual DOM tree — a nested JavaScript object structure representing the whole UI.

Step 2: React Commits to the Real DOM

Once React has built this initial Virtual DOM tree, it performs a commit — it takes the Virtual DOM description and creates the corresponding Real DOM nodes in the browser for the very first time. This initial commit does involve creating Real DOM nodes, which is unavoidable — after all, the browser needs something real to display. But this only happens once on mount.

React attaches the resulting DOM tree to the mount point in your HTML (typically a <div id="root">), and the user sees the page.

At this point, React also keeps a reference to the Virtual DOM tree it just created. This stored snapshot is crucial, because it will be used as the basis for comparison the next time anything changes.


Section 4 — How State or Props Changes Trigger a Re-render

The real power of the Virtual DOM becomes visible when something changes — when a user interacts with the page, when data arrives from an API, or when a timer fires and updates some state.

What Triggers a Re-render?

In React, a component re-renders when:

  • Its own state changes — via useState setter function or this.setState in class components

  • Its props change — because a parent component re-rendered and passed new values down

  • Its context value changes — if the component consumes a React Context

Let us use a concrete example. Imagine a shopping cart component that displays a count of items:

function CartCounter() {
  const [count, setCount] = React.useState(0);

  return (
    <div className="cart">
      <p>Items in cart: {count}</p>
      <button onClick={() => setCount(count + 1)}>Add Item</button>
    </div>
  );
}

When the user clicks "Add Item", setCount is called with the new value. This tells React that the state for CartCounter has changed and that this component needs to re-render.

What React Does NOT Do

Crucially, at this point React does not immediately reach into the Real DOM and update the text inside the <p> tag. That would be the old, expensive imperative approach. Instead, React schedules a re-render and follows a deliberate, controlled process. This is where the Virtual DOM earns its place.


Section 5 — Creating a New Virtual DOM Tree

When a re-render is triggered, React calls the component function again (or the class render method) with the new state or props values. This produces a new Virtual DOM tree — a fresh JavaScript object representation of what the UI should now look like, given the updated data.

Using our cart example, after the state change, the new Virtual DOM tree for CartCounter might look like this (conceptually):

// Previous Virtual DOM (count was 0)
{
  type: 'div',
  props: {
    className: 'cart',
    children: [
      {
        type: 'p',
        props: { children: 'Items in cart: 0' }
      },
      {
        type: 'button',
        props: { children: 'Add Item', onClick: [Function] }
      }
    ]
  }
}

// New Virtual DOM (count is now 1)
{
  type: 'div',
  props: {
    className: 'cart',
    children: [
      {
        type: 'p',
        props: { children: 'Items in cart: 1' }  // <-- This changed
      },
      {
        type: 'button',
        props: { children: 'Add Item', onClick: [Function] }  // <-- Unchanged
      }
    ]
  }
}

React now has two Virtual DOM trees: the previous tree (snapshot from the last render) and the new tree (just generated). The question now becomes: what is actually different between these two trees, and what is the minimum set of Real DOM changes needed to make the UI reflect the new tree?

This is where diffing and reconciliation come in.


Section 6 — Diffing and Reconciliation: Finding What Changed

Reconciliation is the process React uses to compare the old Virtual DOM tree with the new Virtual DOM tree in order to determine what has actually changed. The comparison algorithm React uses is called the diffing algorithm.

What Is Diffing?

Diffing simply means comparing two trees, node by node, to find the differences between them. The result of this comparison is a minimal description of what changes need to be applied to the Real DOM to bring it in line with the new Virtual DOM.

The naive approach to comparing two trees would be to compare every single node against every other possible node — a process that would be astronomically expensive for large trees. Computer science tells us that the general tree diffing problem has a complexity of O(n³), meaning that for a tree with 1,000 nodes, you would need to perform approximately one billion comparisons. That is obviously impractical for a UI framework.

React addresses this with a set of heuristics — practical assumptions about how UI trees typically behave — that reduce the complexity to O(n), meaning the number of comparisons scales linearly with the number of nodes. Let us look at these assumptions.

React's Diffing Heuristics

Heuristic 1: Elements of Different Types Produce Different Trees

If React is comparing a node in the old tree against the corresponding node in the new tree and finds that the element type has changed — for example, a <div> has been replaced by a <section>, or a <p> has been replaced by a <span> — React assumes that the entire subtree rooted at that node has changed completely. It tears down the old subtree and builds the new one from scratch, without attempting to match individual children.

// Old render
<div>
  <Counter />
</div>

// New render — type changed from div to section
<section>
  <Counter />
</section>

In this case, React would unmount the old <div> and its entire subtree (including Counter) and mount a completely new <section> and a new Counter instance.

Heuristic 2: Elements of the Same Type Are Updated in Place

If the element type is the same between the old and new trees, React assumes the node represents the same UI concept and simply looks at what attributes or props have changed, updating only those. The underlying Real DOM node is preserved and updated rather than replaced.

// Old
<p className="text" style={{ color: 'blue' }}>Hello</p>

// New
<p className="text" style={{ color: 'red' }}>Hello</p>

Here, React would keep the existing <p> DOM node and simply update the color style property from 'blue' to 'red'. The text content is unchanged, so it is left alone.

Heuristic 3: The key Prop for Lists

When React encounters a list of elements (for example, a <ul> containing multiple <li> items), it needs a way to match up items between the old and new lists. Without any additional information, React would simply compare items by their position in the list — first item to first item, second to second, and so on.

The problem arises when items are reordered or a new item is inserted at the beginning of the list. Without keys, React might unnecessarily update many items because the positional mapping is incorrect.

The key prop provides React with a stable identity for each list item, allowing it to correctly match items across renders even when the order changes:

// Without keys — React must compare by position, leading to inefficiency
{products.map(product => (
  <ProductCard product={product} />
))}

// With keys — React can correctly match items by identity, not position
{products.map(product => (
  <ProductCard key={product.id} product={product} />
))}

With keys, if an item is moved from position 3 to position 1, React can identify it by its key, recognise it as the same element, and simply reorder the existing Real DOM node rather than destroying and recreating it.


Section 7 — How React Identifies the Minimal Set of Required Changes

Through the diffing process, React builds up a list of specific, targeted changes that need to be made to the Real DOM. This list is sometimes conceptually referred to as a diff or a patch. The key principle is minimality — React aims to perform the smallest possible number of Real DOM operations to bring the UI into the correct state.

Let us walk through a more complete example to illustrate this. Consider a component that renders a user profile:

function UserProfile({ user }) {
  return (
    <div className="profile">
      <img src={user.avatar} alt="avatar" />
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
      <span className="badge">{user.role}</span>
    </div>
  );
}

Suppose the parent re-renders UserProfile with a new user object where only the bio field has changed. React will:

  1. Compare the old Virtual DOM tree with the new Virtual DOM tree

  2. Find that the div node type is unchanged — proceed into its children

  3. Find that the img node type and src prop are unchanged — no Real DOM update needed

  4. Find that the h2 node type and content are unchanged — no Real DOM update needed

  5. Find that the p node type is the same, but the text content has changed — one Real DOM update: update the text node

  6. Find that the span node type and content are unchanged — no Real DOM update needed

The result is a single, targeted update to one text node in the Real DOM. Even though the entire component re-rendered (a new Virtual DOM tree was created for the whole component), only the one thing that actually changed gets touched in the Real DOM. This is the power of diffing.


Section 8 — Updating Only Changed Nodes in the Real DOM

Once React has completed the diffing phase and determined the precise set of changes needed, it moves into what is called the commit phase — the point at which React actually applies those changes to the Real DOM.

The Commit Phase

During the commit phase, React synchronously applies all the identified changes to the Real DOM in a single, batched operation where possible. This is important for performance: rather than applying each change individually and triggering a separate reflow/repaint for each one, React applies them together, giving the browser the opportunity to batch the rendering work.

The types of operations React may perform during the commit phase include:

  • Updating text content — changing the innerText or textContent of a text node

  • Updating attributes or properties — changing className, style, src, value, etc.

  • Inserting new nodes — adding a new DOM element that appeared in the new Virtual DOM but not the old one

  • Removing nodes — removing a DOM element that was in the old Virtual DOM but not the new one

  • Reordering nodes — moving a DOM element to a different position (often using key-based reconciliation)

Crucially, React only performs operations that are strictly necessary. DOM nodes that have not changed are not touched at all.

Batching Updates

React also employs update batching to further improve performance. If multiple state updates happen in quick succession — for example, within a single event handler — React groups them together and processes them as a single re-render rather than triggering a separate re-render for each update.

function handleCheckout() {
  setCartCount(0);       // State update 1
  setOrderPlaced(true);  // State update 2
  setDiscount(0);        // State update 3
}

Without batching, this would trigger three separate re-renders and three separate DOM update cycles. With React's batching (which in React 18 applies automatically in all contexts), these three updates are batched together into a single re-render, and the DOM is updated only once with all three changes applied together.


Section 9 — Why This Approach Improves Performance

At this point, we have covered all the pieces of the Virtual DOM mechanism. Let us now consolidate our understanding of why this approach is meaningfully better for performance than direct, unmanaged DOM manipulation.

Reason 1: Minimising Expensive DOM Operations

As established at the beginning of this article, interactions with the Real DOM are expensive because they can trigger reflow and repaint cycles. The Virtual DOM approach ensures that React only ever performs Real DOM operations for things that have actually changed. In a large component tree with hundreds of elements, a single piece of state changing might only require updating one or two Real DOM nodes. Without the Virtual DOM acting as a buffer and diff engine, naive implementations might update far more of the DOM than necessary.

Reason 2: Working in Fast JavaScript Memory

The process of creating and comparing Virtual DOM trees happens entirely in JavaScript memory. JavaScript object operations are extremely fast — far faster than DOM operations. By doing the "heavy thinking" work in JavaScript (building the new tree, running the diff algorithm) and then performing only the minimal required DOM writes, React shifts the computational cost from the slow DOM layer to the fast JavaScript layer.

Reason 3: Abstracting Manual DOM Management from Developers

Beyond raw performance, the Virtual DOM also improves developer productivity, which indirectly improves application quality and maintainability. Developers declare what they want the UI to look like in any given state, and React handles all the work of figuring out how to get the DOM there. This declarative model prevents the class of bugs that arise from manually mismanaging DOM updates, and it means developers write significantly less imperative, error-prone DOM manipulation code.

Reason 4: Enabling a Declarative Programming Model

The Virtual DOM enables React's declarative programming model — you describe the UI as a function of state (UI = f(state)), and React ensures the Real DOM always reflects the current state. This is conceptually much simpler than the imperative model of manually commanding the DOM to change, and it makes complex UIs far easier to reason about.


Section 10 — The Full React Flow: Render → Diff → Commit

Now that we have built up the full picture, let us consolidate everything into a clear, end-to-end mental model of how React processes changes. This render → diff → commit pipeline is the heart of how React works.

Phase 1: Render Phase

The render phase is where React calls your component functions to produce a new Virtual DOM tree. It is important to understand that "rendering" in React's terminology refers specifically to this process of generating the Virtual DOM description — it does not mean updating the browser screen. The render phase is pure and has no side effects on the DOM.

During this phase:

  • React calls each component function that needs to re-run (because its state or props changed)

  • Each component returns React Elements (the Virtual DOM nodes)

  • React assembles these into a complete new Virtual DOM tree

  • React compares this new tree against the previously stored Virtual DOM tree using the diffing algorithm

  • React builds a list of changes (the "diff" or "patch") that need to be applied

Phase 2: Reconciliation (Diffing)

Reconciliation is technically part of the render phase in terms of timing, but it is worth calling out explicitly as its own conceptual step. This is where React runs the diffing algorithm described in Section 6 and 7 — traversing both the old and new Virtual DOM trees simultaneously, node by node, identifying what has changed, been added, or been removed.

The output of reconciliation is a precise, minimal set of DOM mutations that need to be performed.

Phase 3: Commit Phase

Once reconciliation is complete, React moves to the commit phase. This is the only phase where React actually touches the Real DOM. React applies all the mutations identified in the reconciliation phase to the Real DOM — updating attributes, inserting nodes, removing nodes, updating text content — and when this phase is complete, the browser's DOM accurately reflects the new Virtual DOM tree.

After the commit phase, React also runs lifecycle effects (useEffect hooks and componentDidMount/componentDidUpdate in class components), which allows your code to respond to the DOM having been updated.

The Updated Virtual DOM Snapshot

After the commit phase, React stores the new Virtual DOM tree as the current snapshot. This becomes the previous tree that will be used for comparison the next time a re-render is triggered. And so the cycle continues — every state or props change triggers a new render → diff → commit cycle.

Here is a visual summary of the entire flow:

User action / state change
         |
         v
[RENDER PHASE]
React calls component functions
↓
New Virtual DOM tree is produced
↓
New tree is diffed against old tree
↓
List of changes is calculated

         |
         v
[COMMIT PHASE]
Minimal changes applied to Real DOM
↓
Browser paints updated pixels to screen
↓
New Virtual DOM stored as current snapshot

         |
         v
(Awaiting next state or props change...)

Summary and Key Takeaways

Let us consolidate the core concepts covered in this article into a set of clear, revisable takeaways.

The Real DOM is expensive to update frequently because changes trigger browser reflow and repaint cycles, which are computationally costly — particularly in complex, dynamic UIs.

The Virtual DOM is a lightweight JavaScript object tree that represents the structure and content of your UI. It lives in memory and can be created, compared, and manipulated far faster than the Real DOM.

On initial render, React builds a Virtual DOM tree from your component output and commits it to the Real DOM for the first time. The Virtual DOM snapshot is stored for future comparison.

When state or props change, React re-runs the affected component functions to produce a new Virtual DOM tree. It does not immediately update the Real DOM.

The diffing algorithm compares the old and new Virtual DOM trees node by node using practical heuristics: elements of different types are replaced entirely; elements of the same type are updated in place; and the key prop enables efficient list reconciliation.

Reconciliation produces a minimal list of Real DOM mutations — only the changes that are absolutely necessary to bring the DOM in line with the new Virtual DOM.

The commit phase applies those minimal mutations to the Real DOM in a batched, efficient manner. The browser then repaints only the affected areas.

The overall flow is: Render → Diff (Reconciliation) → Commit, and this cycle repeats every time something changes in your application.

Performance benefits come from doing the expensive thinking work in fast JavaScript memory, minimising actual DOM interactions, and batching updates together — all of which dramatically reduce the number of costly reflow and repaint cycles.


Conclusion

It is worth clarifying one common misconception: the Virtual DOM is not magic, and it is not always the fastest possible approach in absolute terms. For very simple, targeted DOM updates, a hand-written direct DOM manipulation can technically be faster than going through the Virtual DOM layer. The real value of the Virtual DOM is that it provides automatically correct and efficiently managed DOM updates at scale, without requiring developers to manually optimise every interaction. It trades a small overhead in JavaScript processing for a significant reduction in unnecessary and expensive DOM work — a trade-off that pays off enormously in large, complex, data-driven applications.

Understanding how the Virtual DOM works gives you a much deeper appreciation for the choices React makes and the reasons why your React applications behave the way they do. It also gives you the foundation to understand more advanced React concepts — such as React Fiber, concurrent rendering, and memoization with React.memo and useMemo — all of which build directly on top of the reconciliation principles explored in this article.