⚛️ React.js

DOM vs
Virtual DOM

📅 Jun 2026 ⏱ 20 min read 🏷

Every frontend developer uses the DOM daily, but few truly understand why directly manipulating it is expensive, how the Virtual DOM solves this problem, and what actually happens when React 'reconciles' your component tree. Let's demystify it all.

Why This Matters

The DOM (Document Object Model) is the browser's live, in-memory representation of your HTML page. Every element, attribute, and piece of text is a node in a massive tree structure. JavaScript can read and modify this tree — and the browser re-renders accordingly.

The problem? DOM operations are slow. Every change can trigger layout recalculations, style computations, and repaints. When you update 100 list items one by one, the browser potentially re-renders the page 100 times.

The Virtual DOM is React's solution: a lightweight JavaScript copy of the real DOM that lets React batch, diff, and minimize the actual DOM operations. But it's not magic — understanding the mechanics is key to writing performant React apps.

The Virtual DOM is NOT a browser feature. It is a programming concept implemented by libraries like React and Vue. The browser only knows about the real DOM.

Let's start from the fundamentals and work our way up to React's Fiber reconciliation engine.

1. What is the DOM?

The DOM (Document Object Model) is a tree-structured API that the browser creates when it parses your HTML. It represents every element as a 'node' object that JavaScript can read, modify, add, or delete. The DOM is not your HTML file — it is a live, interactive object model built from it.

🌳

The DOM Tree Structure

When the browser loads an HTML file, it parses the markup and builds a tree of Node objects. The root is the `document` object. Every HTML tag becomes an Element node, every piece of text becomes a Text node, and every attribute becomes an Attr node. Parent-child relationships in HTML are mirrored as parent-child relationships in the tree.

html
<!-- This HTML... -->
<div id="app">
  <h1>Hello</h1>
  <p>World</p>
</div>

<!-- ...becomes this DOM tree:
document
  └── html
       ├── head
       └── body
            └── div#app
                 ├── h1
                 │    └── "Hello" (text node)
                 └── p
                      └── "World" (text node)
-->
🔧

DOM Manipulation with JavaScript

JavaScript interacts with the DOM through APIs like `document.getElementById()`, `document.createElement()`, `element.appendChild()`, and `element.innerHTML`. Every call directly mutates the live tree, and the browser must react to each change.

javascript
// Creating and inserting a new element
const newItem = document.createElement('li');
newItem.textContent = 'New Item';
document.getElementById('list').appendChild(newItem);

// Modifying an existing element
document.querySelector('.title').style.color = 'red';
document.querySelector('.title').textContent = 'Updated!';
🐌

Why Direct DOM Manipulation is Expensive

Every DOM change can trigger the browser's rendering pipeline: Style Calculation → Layout (Reflow) → Paint → Composite. If you change an element's width, the browser must recalculate the layout of every element that could be affected. Changing 50 elements in a loop can trigger 50 separate reflow cycles. This is the core performance problem.

2. The Browser Rendering Pipeline

To understand WHY DOM manipulation is slow, you need to understand what the browser does every time the DOM changes. The rendering pipeline is a multi-step process that converts your code into pixels on the screen.

📄

Step 1: Parse HTML → DOM Tree

The browser reads your HTML file top-to-bottom and constructs the DOM tree. Each tag becomes a node object. This happens once on initial page load.

🎨

Step 2: Parse CSS → CSSOM Tree

The browser parses all CSS (stylesheets, inline styles, browser defaults) and builds the CSSOM (CSS Object Model) — a tree of style rules. Each DOM node is matched against CSS selectors to determine its computed styles.

🔗

Step 3: Combine → Render Tree

The browser merges the DOM and CSSOM into a Render Tree. Only visible elements are included — elements with `display: none` are excluded. Each node in the render tree knows its content AND its computed styles.

📐

Step 4: Layout (Reflow)

The browser calculates the exact position and size of every element in the render tree. This is the most expensive step. Changing one element's width can cascade and force the browser to recalculate the layout of its siblings, parents, and children.

🖌️

Step 5: Paint & Composite

Finally, the browser fills in pixels (paint) and combines layers (composite). Changes that only affect `opacity` or `transform` skip layout entirely and only trigger compositing — which is why CSS animations using `transform` are so much faster than animating `width` or `top`.

Every time you modify the DOM, the browser may re-run steps 4 and 5. Batch your changes to avoid triggering multiple reflows!

3. What is the Virtual DOM?

The Virtual DOM is a lightweight, in-memory JavaScript representation of the real DOM. It is a plain JavaScript object tree that mirrors the structure of the actual DOM. React uses it as a staging area — making all changes to this cheap copy first, then efficiently applying only the minimum necessary changes to the real DOM.

📦

It's Just JavaScript Objects

A Virtual DOM node is not a DOM element. It is a simple JavaScript object with properties like `type`, `props`, and `children`. Creating and comparing JavaScript objects is orders of magnitude faster than creating and comparing real DOM nodes, because there's no browser rendering overhead.

jsx
// When you write this JSX:
<div className="card">
  <h1>Hello</h1>
</div>

// React creates this Virtual DOM object:
{
  type: 'div',
  props: { className: 'card' },
  children: [
    {
      type: 'h1',
      props: {},
      children: ['Hello']
    }
  ]
}
🌲🌲

Two Trees at All Times

React maintains TWO virtual DOM trees in memory at all times: the 'current' tree (representing what's on screen right now) and the 'work-in-progress' tree (representing the next update). When state changes, React builds the new tree, diffs it against the current tree, and patches only the differences.

Why Not Just Diff the Real DOM?

Reading properties from real DOM nodes is slow because it forces the browser to flush pending style calculations. Comparing two JavaScript objects in memory is essentially free. The Virtual DOM lets React do all its comparison work without touching the browser at all.

4. Reconciliation: How React Diffs the Trees

Reconciliation is the process React uses to determine which parts of the UI need to change. When your state updates, React generates a new Virtual DOM tree, compares it to the previous one using a 'diffing algorithm', and computes the minimum set of DOM operations needed.

💥

Rule 1: Different Types → Full Rebuild

If the root element type changes (e.g., from `

` to `
`, or from `` to ``), React tears down the entire old subtree (unmounts it) and builds the new one from scratch. This is why changing wrapper element types is expensive.

jsx
// Before: React renders a <div> tree
<div><Counter /></div>

// After: React sees <section> — completely different type!
<section><Counter /></section>
// Result: Old <div> is destroyed. Counter is unmounted and remounted.
// All state inside Counter is LOST.
✏️

Rule 2: Same Type → Update Attributes

If the element type is the same (e.g., both are `

`), React keeps the same DOM node and only updates the changed attributes. For example, if `className` changed from 'old' to 'new', React calls `element.className = 'new'` — one single DOM operation instead of rebuilding the entire node.

jsx
// Before
<div className="old" style={{ color: 'red' }} />

// After
<div className="new" style={{ color: 'blue' }} />

// React does NOT destroy the <div>. It only runs:
// element.className = 'new';
// element.style.color = 'blue';
🔑

Rule 3: Lists Need Keys

When diffing lists of children, React matches elements by their `key` prop. Without keys, React compares by index — which means inserting an item at the beginning forces React to update EVERY subsequent item. With proper keys, React knows exactly which items were added, removed, or moved.

jsx
// BAD: No keys — inserting at top re-renders the entire list
{items.map((item, index) => <li>{item}</li>)}

// GOOD: Stable keys — React tracks each item individually
{items.map(item => <li key={item.id}>{item.name}</li>)}

// TERRIBLE: Using index as key when list order can change
{items.map((item, i) => <li key={i}>{item}</li>)} // DON'T!

Never use array index as a key if the list can be reordered, filtered, or items can be inserted/deleted. Use a stable, unique identifier like a database ID.

5. React Fiber: The Engine Behind It All

React Fiber is the internal reconciliation engine introduced in React 16. It replaced the old 'stack reconciler' which processed updates synchronously (blocking the main thread). Fiber breaks rendering work into small units called 'fibers' that can be paused, resumed, or abandoned.

🧊

The Problem Fiber Solves

The old stack reconciler processed the entire component tree in one synchronous pass. If you had 10,000 list items to update, the browser was frozen until React finished diffing and patching all of them. Animations would stutter, typing would lag, and the UI felt unresponsive.

Incremental Rendering

Fiber splits the work into small chunks (one fiber per component). After processing each chunk, React checks: 'Is there something more urgent to do?' (like handling user input). If yes, it pauses the current work and handles the urgent task first. This is the foundation of React 18's concurrent features.

🎯

Priority Levels

Fiber assigns priority levels to different types of updates. User input (typing, clicking) gets the highest priority. Data fetching results get medium priority. Off-screen rendering gets the lowest. This is how `useTransition` and `useDeferredValue` work under the hood.

6. DOM vs Virtual DOM: Head-to-Head

Let's compare the two approaches directly to crystallize the differences.

Feature Real DOM Virtual DOM
What is it? Browser's live object model of the page Lightweight JS copy of the DOM tree
Update speed Slow (triggers reflow/repaint) Fast (JS object diffing)
Memory usage Heavy (full node objects) Light (plain JS objects)
Direct manipulation Yes No (React handles it)
Batching Manual Automatic
Used by All browsers natively React, Vue, Preact

7. Real-World Example: Updating a List

Let's see the actual difference in code. Imagine you need to update one item in a list of 1,000 items.

📜

Vanilla JS (Direct DOM)

With vanilla JavaScript, the naive approach rebuilds the entire list by setting `innerHTML`. This destroys all 1,000 existing DOM nodes and creates 1,000 new ones — triggering a massive reflow. The smarter approach targets only the changed node, but you have to manually track which one changed.

javascript
// ❌ NAIVE: Rebuilds the ENTIRE list (1,000 DOM nodes destroyed + recreated)
list.innerHTML = items.map(i => `<li>${i.text}</li>`).join('');

// ✅ SMART: Only update the single changed item
const targetLi = list.children[changedIndex];
targetLi.textContent = newText;
// But YOU must track which index changed — error-prone in complex UIs
⚛️

React (Virtual DOM)

With React, you simply update the state. React's diffing algorithm automatically detects that only ONE item changed in the list. It generates a single DOM operation to update that specific `

  • ` element. You write declarative code; React handles the optimization.

    jsx
    // You just update state — React figures out the minimal DOM changes
    setItems(prev =>
      prev.map((item, i) =>
        i === changedIndex ? { ...item, text: newText } : item
      )
    );
    
    // React's diff result: "Only item at index 42 changed"
    // React runs: list.children[42].textContent = newText;
    // The other 999 <li> elements are completely untouched!
  • 8. Common Misconceptions

    There are several myths about the Virtual DOM that even experienced developers believe. Let's bust them.

    Myth: Virtual DOM is Always Faster

    The Virtual DOM adds overhead — React must create the virtual tree, diff it, and then apply changes to the real DOM. For simple, targeted updates (like changing one element's text), direct DOM manipulation with `element.textContent = 'new'` is actually faster. The Virtual DOM wins when updates are complex and scattered across many components.

    Myth: React Never Touches the Real DOM

    React absolutely touches the real DOM — that's how pixels get on your screen! The Virtual DOM is just an intermediary. React calculates the MINIMUM set of real DOM operations needed and executes them in a batch. It's not avoiding the DOM, it's minimizing contact with it.

    Myth: Virtual DOM is a React Invention

    While React popularized the concept, the idea of diffing a virtual representation against a real one existed before React. Vue.js, Preact, Inferno, and other frameworks also use virtual DOM implementations. Svelte takes a completely different approach by compiling away the virtual DOM entirely at build time.

    Truth: It's About Developer Experience

    The real value of the Virtual DOM isn't raw speed — it's that developers can write simple, declarative UI code (describe WHAT the UI should look like) and let the framework figure out HOW to efficiently update the real DOM. This is a massive productivity and maintainability win.