AlgoInsight

Free lesson · React Deep Dive

What the Virtual DOM Is

Medium8 min readreactjavascriptfrontend

Lesson 14.1 - What the Virtual DOM Is

The vocabulary lesson.

The term "Virtual DOM" was Facebook's framing when React launched in 2013. It served a marketing purpose - differentiating React from the jQuery and Backbone approaches that mutated the DOM directly. The pitch was: "we'll maintain a virtual representation of the DOM in JavaScript, diff it efficiently, and only update the real DOM where needed."

The pitch worked. React became the dominant frontend framework. But the term "Virtual DOM" obscures what's actually happening, and modern frontend has moved past treating it as a magical performance feature. Several leading frameworks - Svelte, SolidJS, Vue 3 (in some modes) - achieve excellent performance with very different approaches. The VDOM isn't the reason React is fast; the reconciliation algorithm built on top of it is.

This lesson covers what's actually in memory when React works - element trees, the fiber tree, and the relationship to the actual DOM. By the end, you'll know what each piece is and the role each plays in React's runtime.


What's Actually in Memory

Three distinct data structures matter:

flowchart TB
    subgraph JS["JavaScript Memory"]
        ElementTree["Element tree<br/>(returned from your<br/>component functions)"]
        FiberTree["Fiber tree<br/>(React's internal<br/>working data structure)"]
    end
    
    subgraph Browser["Browser Memory"]
        DOM["DOM tree<br/>(the actual HTML elements<br/>the browser renders)"]
    end
    
    ElementTree -.->|"reconciler builds/updates"| FiberTree
    FiberTree -.->|"commit phase applies"| DOM

    style ElementTree fill:#fbbf24,stroke:#f59e0b,color:#000
    style FiberTree fill:#a78bfa,stroke:#8b5cf6,color:#000
    style DOM fill:#34d399,stroke:#10b981,color:#000
A diagram of the three data structures involved in React's rendering. In JavaScript memory, two structures exist. First, the element tree  - the result of your component functions returning JSX. This is just plain JavaScript objects describing what the UI should look like. Second, the fiber tree  - React's internal working data structure. The fiber tree is what React uses to track work, schedule rendering, and coordinate the lifecycle. The reconciler updates the fiber tree based on the element tree from each render. In browser memory, the actual DOM tree exists  - the real HTML elements that the browser renders to pixels. The commit phase of React's lifecycle applies changes from the fiber tree to the DOM tree. The "Virtual DOM" in popular discourse usually refers to the element tree, but the fiber tree is the more accurate technical concept  - it's the in-memory representation React actually uses for its work.

Let me walk through each in detail.

Element Tree

The element tree is what your component functions return. JSX compiles to React.createElement calls, which return plain objects:

function Greeting() {
  return <div className="greeting">Hello, world!</div>
}

When Greeting() is called, the return value is:

{
  type: 'div',
  props: { className: 'greeting', children: 'Hello, world!' },
  // ...other fields
}

For nested JSX, the result is a tree of these objects:

return (
  <div>
    <h1>Title</h1>
    <p>Body</p>
  </div>
)

Produces:

{
  type: 'div',
  props: {
    children: [
      { type: 'h1', props: { children: 'Title' } },
      { type: 'p', props: { children: 'Body' } },
    ]
  }
}

The element tree is what each render produces. It's transient - created fresh each render, used by React's reconciler, then thrown away. The next render produces a new element tree.

This is what most discourse means by "Virtual DOM." The plain-object tree React diffs each render.

Fiber Tree

The fiber tree is React's internal data structure for tracking work. It's more elaborate than the element tree:

The fiber tree is what's really "the VDOM" in the technical sense. The element tree is the input to reconciliation; the fiber tree is the state the reconciler operates on.

Most engineers don't need to know about fibers directly. But knowing they exist explains why React has the properties it has:

DOM Tree

The DOM tree is the browser's actual HTML representation. It's what gets painted to pixels. React's commit phase modifies the DOM to match the latest reconciled state.

The DOM tree is real-world; React doesn't control it. React just sends mutation commands (appendChild, setAttribute, etc.) and the browser updates its internal representation.

For most React work, the DOM is the destination. The element and fiber trees are means to the end of producing the right DOM.


The Relationship Between Trees

The shape of the three trees is similar but not identical:

Element tree → Fiber tree: Mostly 1:1. Each element corresponds to a fiber. Some details differ - fragments are special, suspense boundaries are tracked, portals are handled with special fiber types.

Fiber tree → DOM tree: Components don't correspond to DOM nodes; only host elements (<div>, <button>, etc.) do. The fiber tree has more nodes than the DOM tree because components are nodes too:

function App() {
  return (
    <Header>
      <Logo />
    </Header>
  )
}

function Header({ children }) {
  return <header>{children}</header>
}

function Logo() {
  return <img src="..." />
}

The fiber tree has: App → Header (component) → header (host) → Logo (component) → img (host).

The DOM tree has just: header → img.

The component fibers don't produce DOM nodes; they produce children that eventually produce DOM nodes (or null, or fragments, etc.).


Why React Uses This Approach

Some specific properties this architecture enables:

Decoupling render from commit. Your component functions don't directly touch the DOM. They return descriptions. React decides when and how to apply them. This is what enables concurrent rendering - React can pause between render and commit.

Batching. Multiple state updates can produce multiple element trees that React batches into one commit. The element-tree-as-intermediate gives a place to coalesce.

Cross-platform. The same React component can render to web (DOM), native (iOS, Android), terminal (Ink), or anywhere. The renderer (react-dom, react-native, etc.) translates the fiber tree to the target.

Time-travel debugging. Tools can capture element trees and replay them. This is what enables React DevTools' "see what rendered when" features.

Concurrent rendering. From M10.6, the ability to pause renders depends on having a separate work tree (fibers) that React can abandon and re-do.

These properties justify the architecture. They wouldn't all be possible with direct DOM manipulation.


Why "Virtual DOM" Is Partly Marketing

A specific point worth landing.

The narrative was: "Virtual DOM is faster because it minimizes DOM operations." This was partially true at the time (compared to jQuery rebuilding entire DOM sections on every update). It's not true as a general claim today.

Svelte compiles components to plain JavaScript that mutates the DOM directly. No VDOM. Often faster than React.

SolidJS uses fine-grained reactivity via signals. Updates touch only the specific DOM nodes whose values changed. No VDOM. Generally faster than React for many workloads.

Vue 3 uses a hybrid approach with finer-grained dependency tracking. The "VDOM" exists but is smaller and used less.

What React's VDOM-and-reconciliation architecture is actually good at:

What it's not particularly good at:

The honest framing: React's architecture is a developer-ergonomics optimization that achieves acceptable (often very good) runtime performance. Other frameworks make different trade-offs. The "VDOM" branding from 2013 overstated the performance argument; the underlying technical contribution is the reconciliation algorithm and the lifecycle, not the VDOM itself.

This isn't a criticism of React - it's a clarification. Knowing what your tools are actually good at (and not) helps you use them well and reach for alternatives when appropriate.


The "Two Trees" Mental Model for Reconciliation

For the rest of this module, the working mental model is:

React maintains two element trees at any moment - the previous committed tree, and the new tree from the most recent render. Reconciliation is the process of computing the differences between them and producing the DOM operations needed to update.

This is simplified - in reality, the fiber tree is the persistent structure and element trees are inputs - but the simplification is useful for understanding the algorithm. The reconciler's job is comparing two trees and producing the diff.

In the next lesson, we'll see how that diff is computed.


A Note on React Internals Documentation

The fiber tree and the reconciler aren't extensively documented in React's user-facing docs. The architecture details come from:

For most application work, you don't need to know the internals at this depth. But for performance work, library development, or debugging hard cases, knowing the underlying architecture helps.

I'll reference these in this module where appropriate. The depth we'll go to is "enough to predict React's behavior in unusual situations" - not "enough to contribute to React's internals."


What I Want You to Take From This Lesson

Six things.

One: Three data structures matter - element tree, fiber tree, DOM tree. Each has a specific role.

Two: The element tree is what your components return. Plain JS objects from React.createElement.

Three: The fiber tree is React's persistent internal data structure. It's what enables the lifecycle features.

Four: The DOM tree is what the browser renders. React's commit phase keeps it in sync with the fiber tree.

Five: "Virtual DOM" is partly marketing. The actual contribution is the reconciliation algorithm and lifecycle, not the VDOM itself. Other frameworks are sometimes faster without one.

Six: For reconciliation, think "two element trees being diffed." The simplification is useful even though fibers are the underlying reality.


Try It

Exercise: Warmup (15 minutes). In a component, log the result of a JSX expression: console.log(<div>Hello</div>). Inspect the plain object that's produced. Note the type and props fields.

Exercise: Core (25 minutes). Use React DevTools (from M13.5) to inspect a component's fiber. The "Tree" view shows the fiber tree structure. Note how it differs from the DOM tree - component fibers don't produce DOM nodes directly.

Exercise: Core (25 minutes). Read Lin Clark's "A Cartoon Intro to Fiber". Note the parts that overlap with this lesson and the parts that go deeper.

Exercise: Stretch (45 minutes). Compare a React app's bundle size to a Svelte equivalent. Note the difference in shipped JavaScript. Read about Svelte's compile-to-DOM approach. Understand why it doesn't need a VDOM.


What's Next

Lesson 14.2 covers the reconciliation algorithm - how React decides what DOM changes to make given two element trees.

Back to React Deep Dive