Articles

What Is the Virtual DOM? How React Updates the UI

The virtual DOM is an in-memory copy of the UI tree that frameworks diff against the previous version to batch and minimize real DOM updates.

Takina Takina · · 4 min read
Abstract illustration representing frontend UI components

The virtual DOM is a lightweight, in-memory representation of a UI tree, kept as plain JavaScript objects instead of real browser elements. Frameworks like React build a new virtual DOM tree on every update, compare it against the previous one, and apply only the differences to the actual DOM. It’s a strategy for making UI updates fast and predictable without hand-optimizing every change yourself.

Why the real DOM is slow to update directly

The browser’s DOM is not just a data structure — every read or write can trigger layout recalculation, style resolution, and repainting. Touching the DOM repeatedly and naively (say, updating a hundred list items one at a time in a loop) can force the browser to redo expensive layout work over and over. Frameworks built before the virtual DOM approach, and plenty of hand-written jQuery-era code, ran into this: DOM writes are the bottleneck, not JavaScript computation.

The virtual DOM sidesteps this by treating DOM updates as a batch problem. Instead of applying every change as soon as it’s known, the framework collects all the changes a render would produce, figures out the minimal set of real DOM operations needed, and applies them together.

How the diffing process works

  1. Render. Your component code runs and produces a new virtual DOM tree — a tree of plain objects describing what the UI should look like, not real elements.
  2. Diff. The framework compares the new tree against the previous render’s tree, node by node, to find what changed. This comparison is called reconciliation.
  3. Commit. The framework applies only the calculated differences to the real DOM — adding, removing, or updating specific nodes and attributes, rather than re-rendering everything from scratch.

Because virtual DOM nodes are plain objects, creating and comparing them is cheap relative to touching the real DOM. The framework can afford to re-run this process on every state change without the cost that direct, unbatched DOM manipulation would carry.

Diffing algorithms rely on heuristics to stay fast rather than doing a fully general tree comparison, which is computationally expensive at scale. React’s reconciler, for example, assumes elements of different types produce different subtrees (so it tears down and rebuilds rather than trying to match them) and uses key props to track list items across re-renders instead of comparing them positionally. Missing or unstable keys is one of the most common sources of subtle rendering bugs in React apps, because the diffing algorithm falls back to matching by position.

Virtual DOM vs signals vs no diffing

The virtual DOM isn’t the only strategy for efficient UI updates, and it’s increasingly not even the dominant one for new frameworks.

ApproachHow it decides what to updateExamples
Virtual DOM diffingRe-render, then diff old vs. new treeReact
Fine-grained reactivity (signals)Track exactly which values changed and update only their dependentsSignals-based frameworks, SolidJS
Compile-time reactivityAnalyze the component at build time and generate direct DOM update codeSvelte

Frameworks using signals skip the diffing step entirely: instead of re-rendering a component and comparing trees, a signal knows exactly which DOM node depends on it and updates that node directly when the value changes. This can be faster than virtual DOM diffing because there’s no tree-walking overhead — but it requires more explicit dependency tracking in how you write components. Svelte takes a related but distinct approach, doing much of this analysis at compile time rather than at runtime.

Where the virtual DOM shows up beyond React

The pattern predates and outlives any one framework. It’s a general technique for computing efficient updates to a tree-shaped UI, and it composes with other rendering strategies. React Server Components, for instance, still produce a tree that gets reconciled on the client for the interactive parts, even though the server-rendered parts skip client-side diffing entirely. Understanding hydration — the process of attaching event listeners and reactivity to server-rendered HTML — also depends on understanding what the virtual DOM is doing under the hood, since hydration typically involves building a virtual tree and reconciling it against markup that already exists.

Trade-offs worth knowing

The virtual DOM isn’t free. Building and diffing a tree on every render has real overhead, even if it’s cheaper than naive direct DOM manipulation. For components that update extremely frequently — animations, high-frequency data visualizations — the diffing overhead itself can become the bottleneck, which is one reason frameworks provide escape hatches (refs, direct DOM access) for those cases. It’s also an abstraction: developers who never learn what’s happening underneath can write code that triggers far more re-rendering and diffing than necessary, because the cost is invisible until a profiler shows it.

The takeaway

The virtual DOM batches UI updates by rendering a new in-memory tree, diffing it against the last one, and applying only the resulting changes to the real DOM — trading a small amount of JavaScript overhead for a large reduction in expensive DOM operations. It was the dominant strategy behind React’s performance model for years, though newer approaches like fine-grained reactivity and compile-time analysis increasingly skip the diffing step altogether. Knowing which model your framework uses explains a lot about how to write components that render efficiently.

Takina Takina · · 5 min read

Promise.all() vs allSettled() vs race() Compared

Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

JavaScript Spread vs Rest Operators, Explained

The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

ResizeObserver API Explained: Watching Element Size

The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.

#Web Development #JavaScript #Frontend