Search

What Is the Virtual DOM and Why Did React Use It?

The short answer

Quick answer: The virtual DOM is a lightweight copy of the page's structure, held in memory as plain JavaScript objects. In React, when your data changes, your components run again and produce a new virtual tree. React compares it with the previous one (diffing), works out the smallest set of changes, and applies only those to the real DOM. The purpose was never raw speed. It lets developers write the interface as a simple function of state, "given this data, the screen looks like this", and leave the tricky job of updating the real page efficiently to the library.

The problem it solved

Before libraries like React, updating a page meant manual DOM manipulation:

// Something changed. Now find and update every affected part by hand.
document.querySelector("#count").textContent = items.length;
const li = document.createElement("li");
li.textContent = newItem.name;
document.querySelector("#list").appendChild(li);
if (items.length > 0) document.querySelector("#empty").remove();

Each change to the data required the right matching changes to the page. As applications grew, the data and the screen drifted out of step, producing bugs like a counter that says 3 above a list of 4.

The simplest fix would be to throw the whole page away and rebuild it from the data on every change. That is always correct, and far too costly: rebuilding the real DOM is slow, and it destroys scroll position, focus and whatever the user was typing.

The virtual DOM gives you the simplicity of "rebuild everything" at the cost of "change only what is needed".

UI as a function of state

With React, you describe what the interface should look like for any given state:

function TodoList({ items }) {
  return (
    <div>
      <p>{items.length} items</p>
      {items.length === 0 ? (
        <p>Nothing to do</p>
      ) : (
        <ul>
          {items.map(item => <li key={item.id}>{item.name}</li>)}
        </ul>
      )}
    </div>
  );
}

There are no instructions for how to get from one screen to the next. This is declarative programming, summed up as UI = f(state).

That JSX is not HTML. It compiles to function calls that return plain objects describing elements:

{ type: "p", props: { children: "3 items" } }

A tree of such objects is the virtual DOM. Creating thousands of small objects is cheap. Touching the real DOM can trigger style recalculation, layout and paint; see how browsers render a web page.

The three phases

The React documentation on render and commit describes the cycle.

  1. Trigger. State changes, for example through setState.
  2. Render. React calls the affected components. They return a new virtual tree. Nothing on screen has changed yet.
  3. Commit. React compares the new tree with the old one and applies the differences to the real DOM.

Note the terminology: in React, "render" means calling your component functions, not drawing to the screen.

How the diff works

Comparing two arbitrary trees perfectly is very expensive. React's reconciliation algorithm uses two assumptions to make it fast, taking time proportional to the number of elements.

1. Elements of different types produce different trees. If a <div> becomes a <span>, or one component type is replaced by another, React does not look for similarities. It discards the old subtree and builds a new one.

2. Elements of the same type are updated in place. React keeps the existing DOM node and changes only the attributes that differ.

For lists, it needs help. That is what key is for.

Why keys matter

Suppose a list has items A, B, C, and a new item Z is added at the front.

Without keys, React compares by position: slot 1 was A and is now Z, slot 2 was B and is now A, and so on. It rewrites every item and adds one at the end.

With stable keys, React matches by identity: A, B and C are unchanged, and Z is new. It inserts one node.

{items.map(item => <li key={item.id}>{item.name}</li>)}

Two rules:

  • Keys must be stable and unique among siblings. Use an ID from your data.
  • Do not use the array index as a key for lists that can be reordered, filtered or inserted into. Since the index of each item changes, React associates state with the wrong item. A classic symptom is text typed into one row appearing in another.

Is the virtual DOM fast?

This is the most common misunderstanding. The virtual DOM is not faster than well-targeted manual DOM updates. It adds work: building a new tree and comparing it with the old one.

What it is faster than is the naive alternative of rebuilding the real DOM every time. Its real value is good enough performance with a much simpler programming model.

The cost becomes noticeable when large trees re-render often. When a component's state changes, React by default re-runs that component and all of its descendants. Developers manage this with:

  • React.memo, to skip a component whose props have not changed.
  • useMemo and useCallback, to keep values and functions stable between renders.
  • Keeping state close to where it is used.
  • Virtualised lists, which render only the rows currently visible.

React's own compiler now automates much of this memoisation.

These checks depend on comparing references, which is why React asks you to treat state as immutable: a changed value must be a new object. See why immutability makes code easier to reason about.

What changed inside React

  • Fiber (2017) rewrote the internals so that rendering can be split into chunks, paused and resumed, instead of blocking the main thread in one go.
  • Concurrent rendering lets urgent updates, such as typing, interrupt less urgent ones.
  • Server components run on the server and send their output, so their code never reaches the browser. See server-side vs client-side rendering.

The alternatives

Rich Harris, the creator of Svelte, argued in Virtual DOM is pure overhead that diffing is unnecessary work if you can know in advance what will change.

ApproachHow updates happenExamples
Virtual DOMRe-run components, diff trees, patchReact, Preact, Vue (with compiler optimisations)
Compile-timeA compiler turns components into code that updates exactly the right DOM nodesSvelte
Fine-grained reactivity (signals)Each piece of state tracks which DOM nodes depend on it and updates only thoseSolidJS, Vue's reactivity, Angular signals, Svelte 5

Signals are the notable trend. A signal is a value that knows who reads it. When it changes, only the specific text nodes or attributes that depend on it are updated, with no component re-run and no tree comparison.

The trade-offs are real on both sides. The virtual DOM gives a simple mental model, in which a component is just a function, and makes it easy to render to targets other than the browser, such as native mobile views. Signals and compilers do less work at run time and ship less code, in exchange for some extra rules about how reactivity behaves.

For most applications, any of these is fast enough. The choice is more often about ecosystem, team experience and tooling.

Common mistakes

  • Index keys on dynamic lists.
  • Mutating state directly, so React sees the same reference and does not re-render.
  • Creating new objects or functions inline on every render and passing them to memoised children, which defeats the memoisation.
  • Putting all state at the top of the tree, so everything re-renders on every change.
  • Optimising before measuring. Use the React Profiler to find what is really slow.

Frequently asked questions

What is the virtual DOM in simple terms?

An in-memory representation of the page as JavaScript objects. A library compares a new version with the old one and updates only the parts of the real page that changed.

Is the virtual DOM faster than the real DOM?

Not inherently. It adds overhead compared with precise manual updates. Its benefit is letting you write simpler code while keeping updates efficient enough.

Why does React need keys?

Keys let React identify which list items are the same between renders, so it can move, keep or remove the right ones instead of rewriting them all.

Do all frameworks use a virtual DOM?

No. Svelte compiles components to direct DOM updates, and SolidJS and others use signals for fine-grained updates.

Conclusion

The virtual DOM made it practical to describe an interface as a function of its data and let the library work out the changes. It was a means to that end, not a performance trick. Newer approaches reach the same goal with less run-time work, but the central idea React popularised, declarative UI driven by state, is now how almost every front-end framework works.

Related articles

Sources and further reading

Usama Muneer

Usama Muneer

Coder, Blogger, Tech Speaker & Web Technologies Enthusiast. Passionate about working on open-source Programming languages & Tools while utilizing my Product Development skills.

Your experience on this site will be improved by allowing cookies Cookie Policy