alion tech studio
  • services
  • faq
  • insights
  • about
  • contact
← back to insights
  1. home
  2. insights
  3. demystifying the virtual dom
jun 11, 2026 • 11 min read • frontend

demystifying the virtual dom: how modern frameworks render ui

By Alex I

The Virtual DOM is probably the most repeated and least understood idea in front-end development. It usually gets introduced as "the thing that makes React fast," which is both an oversimplification and, in a strict sense, wrong — a Virtual DOM is almost never faster than a hand-written, surgical DOM update. Its real value is something else entirely: it lets you write UI declaratively and hands the tedious, error-prone job of synchronising the screen to the framework. This article unpacks how that actually works, what it costs, and why a wave of 2026 frameworks is quietly abandoning it.

the problem it solves isn't "the dom is slow"

The common explanation — "the DOM is slow, so we avoid touching it" — is misleading. Reading and writing DOM nodes is fast. What is expensive is the work the browser does afterward: recalculating styles, running layout (reflow), and painting. And crucially, that cost is triggered by uncoordinated updates. If your code changes ten things in a loop and reads layout in between, you can force ten synchronous reflows. The browser is not the villain; uncoordinated mutation is.

So the actual problem a framework needs to solve is: given a description of what the UI should look like, apply the smallest, best-batched set of changes to make the real DOM match — without the developer hand-writing that bookkeeping. The Virtual DOM is one answer to that problem.

what the virtual dom actually is

A Virtual DOM is just a tree of plain JavaScript objects describing your UI. When you write JSX like <li className="item">Hello</li>, it compiles to a function call that returns an object roughly like this:

{
  type: "li",
  props: { className: "item" },
  children: ["Hello"]
}

That object is cheap to create and cheap to throw away. It is not on the screen; it is a description. Nothing has touched the real DOM yet. This is the key mental shift: you are no longer issuing commands to the page, you are producing a value that represents the page.

the render / diff / commit cycle

Every update flows through three phases. Understanding them explains almost every performance question you will ever have about a framework like React:

  1. Render. State changes, so the framework calls your components and builds a brand-new Virtual DOM tree describing the next UI. This is why "re-render" does not mean "touch the DOM" — it means "run your function and produce a new description."
  2. Diff (reconciliation). The framework compares the new tree against the previous one, node by node, to find what actually changed.
  3. Commit. Only the real differences are written to the DOM, batched into as few operations as possible.

The expensive browser work happens once, in commit, on the minimum set of nodes — instead of repeatedly as a side effect of your logic.

PREVIOUS TREE (kept in memory) <ul> li · milk key="m1" li · eggs key="e2" li · bread key="b3" NEXT TREE (fresh render) <ul> li · milk key="m1" li · eggs ✓ key="e2" · class=done li · bread key="b3" diff patch: 1 operation node("e2").className = "done" real dom — commit 1 mutation → 1 style recalc → 1 paint. milk and bread are never touched.
One state change (marking "eggs" as done) flowing through the cycle. The render phase produced a whole new tree of cheap objects, but the diff — guided by stable keys — reduced all of it to a single DOM mutation. The browser's expensive layout-and-paint work runs once, on one node.

why diffing is fast enough: the heuristics

Comparing two arbitrary trees is, in the general case, an O(n³) problem — hopelessly slow for a UI. Frameworks make it practical by cheating with two assumptions that hold almost all the time, bringing it down to roughly O(n):

  • Different element types produce different trees. If a node was a <div> and is now a <section>, the framework does not try to reconcile their children — it tears the old subtree down and builds the new one. Cheap to decide, occasionally wasteful, almost always correct.
  • Children in a list are identified by a stable key. Keys let the diff match an item to its previous self across reorders, instead of comparing by position.

the keys mistake everyone makes

Because keys drive list reconciliation, getting them wrong causes some of the most baffling bugs in front-end work. The classic error is using the array index as a key:

{items.map((item, i) => <Row key={i} ... />)}  // fragile

If you delete the first item, every remaining item's index shifts by one. The framework now thinks every row "changed," so it reuses the wrong DOM nodes — and any internal state, like the text in an uncontrolled input or a checkbox's checked status, stays attached to the wrong row. Use a stable identifier from your data (item.id), and index-as-key becomes safe only for static lists that never reorder. This single rule prevents a whole category of "why is my input showing the wrong value" tickets.

watching it happen in devtools

None of this needs to stay theoretical — the render/diff/commit cycle is directly observable, and checking it is the fastest way to build correct intuition. In React DevTools, the Profiler tab records which components rendered during an interaction, how long each took, and — with "record why each component rendered" enabled — whether it was props, state, or a parent's render that caused it. The classic discovery on first profile is a component near the root re-rendering on every keystroke because an object or callback prop is recreated each time, dragging its entire subtree through the render phase for nothing.

The browser side has its own view: in Chrome DevTools, the Rendering panel's "paint flashing" highlights exactly which pixels repaint after each update. A well-behaved commit flashes one small region; a mis-keyed list flashes every row. Watching the two panels together — what the framework re-rendered versus what the browser actually repainted — makes the distinction between the cheap phase and the expensive phase concrete in about five minutes of clicking around your own app.

the virtual dom is not free

Honesty about the trade-off is what separates understanding from cargo-culting. The Virtual DOM costs you two things: memory, because you keep a copy of the UI tree around to diff against, and CPU, because every update re-runs components and walks the tree even when little changed. In a large app, an unmemoised component high in the tree can re-render thousands of descendants on every keystroke. That is why React ships memo, useMemo, and useCallback — they exist precisely to stop the Virtual DOM from doing redundant work. The fact that a framework needs escape hatches to skip re-rendering is the clearest sign that the VDOM is an overhead you manage, not a free speed-up.

React's Fiber architecture addresses a related problem: a big render could block the main thread and hurt responsiveness — the kind of delay measured by INP, one of the Core Web Vitals. Fiber makes reconciliation interruptible, so urgent work like typing can preempt a large, low-priority update.

where server rendering fits in

The Virtual DOM also explains how modern server-side rendering works, because SSR is essentially the render phase running on the server: components execute, produce the description, and it is serialised to HTML instead of being diffed against a live tree. The browser can paint that HTML immediately — which is great for LCP — but the page is not interactive yet. Hydration is the step that closes the gap: the framework re-runs the render phase in the browser, builds the Virtual DOM it expects the HTML to represent, and attaches event listeners to the existing nodes rather than recreating them.

That is why hydration mismatches ("text content does not match server-rendered HTML") are a Virtual DOM phenomenon: the tree rendered on the client disagreed with the HTML from the server — usually because of timestamps, locale formatting, or browser-only branches — and the framework can no longer trust that the description and the reality line up. It is also why hydration has a real CPU cost on large pages, and why newer approaches (islands, resumability, streaming SSR from the edge) all aim to run less of the render phase in the browser. The mental model stays the same: somewhere, a description of the UI has to be produced and reconciled with what the user is looking at.

2026: the frameworks skipping the vdom

The most interesting development is that the newest frameworks increasingly do not use a Virtual DOM at all. Signals-based, fine-grained reactivity — popularised by SolidJS and now adopted by Svelte 5's runes, Vue's Vapor mode, and Angular signals — takes a different bet. Instead of re-running a component and diffing its whole output, a signal wires each piece of state directly to the exact DOM node that depends on it. When the value changes, only that node updates; there is no tree to rebuild and no diff to compute.

This trades a little authoring ceremony for a model that often does less work and uses less memory, especially under frequent updates. It does not make the Virtual DOM obsolete — React's enormous ecosystem and mental model are not going anywhere — but it reframes the VDOM as one solution among several, rather than the definition of how modern UI works.

the takeaway

The Virtual DOM was never really about speed; it was about letting you describe the UI as a function of state and trusting the framework to reconcile reality with that description. Knowing the render/diff/commit cycle tells you why re-renders happen and how to contain them; knowing the cost tells you when to reach for memoisation or a finer-grained alternative. Understand the mechanism, and the framework stops being magic and starts being a tool you can reason about.

AX

Alex I

Software engineer and founder of alion tech studio. Writes and consults on web performance, security, mobile apps, backend systems, cloud infrastructure, and fullstack architecture.

related reading

jun 13, 2026 • 11 min read • frontend

mastering css grid: a practical guide to two-dimensional layout

read article →
jun 5, 2026 • 12 min read • performance

webassembly in practice: where it wins, what it costs

read article →
alion tech studio

an independent software engineering studio. we write about performance, security, and building for the web, and consult on the same.

navigation

  • services
  • faq
  • insights
  • about
  • contact

legal

  • privacy policy
  • terms of service

© 2026 alion tech studio. all rights reserved.