Understanding Virtual DOM and Why Svelte/SolidJS Opt Out
Wenhao Wang
Dev Intern · Leapcell

Introduction
The landscape of frontend development has been dramatically reshaped by a new generation of JavaScript frameworks. For years, React and Vue, with their innovative use of the Virtual DOM, have dominated the conversation, promising efficiency and developer-friendly abstractions. The Virtual DOM became an almost ubiquitous optimization technique, widely accepted as a best practice for managing UI updates. However, as the frontend ecosystem continues to evolve, new players like Svelte and SolidJS are challenging this established paradigm. They argue that the Virtual DOM, while clever, introduces its own set of complexities and overhead, proposing alternative strategies that aim for even greater performance and simplicity. This discussion is not merely academic; it has significant implications for how we build and optimize web applications today and in the future. Let's dive into what the Virtual DOM actually is and why these newer frameworks believe we can achieve more without it.
The Core Concepts
Before we explore the alternatives, it's crucial to understand the foundational technologies at play.
The DOM (Document Object Model)
The DOM is a programming interface for web documents. It represents the page structure in a tree-like fashion, where each node is an object representing a part of the document (e.g., an HTML element, an attribute, or a text node). Interacting with the DOM, such as adding, removing, or updating elements, directly reflects changes on the user's screen. Directly manipulating the DOM can be slow, especially when many changes occur in rapid succession, as it often triggers layout recalculations and repaints by the browser.
The Virtual DOM
The Virtual DOM is a lightweight, in-memory representation of the actual DOM. When an application's state changes, a new Virtual DOM tree is created. This new tree is then compared to the previous one in a process called "diffing." The diffing algorithm identifies the minimal set of changes needed to update the actual DOM. These changes are then batched and applied to the real DOM in a single, optimized operation. This minimizes direct DOM manipulations, aiming to improve performance.
Consider a simple component that displays a counter.
// React-like pseudocode function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); }
When setCount is called, React would:
- Re-render the
Countercomponent, producing a new Virtual DOM tree. - Diff this new tree against the previous one.
- Identify that only the text content within the
<p>tag has changed. - Update only that specific text node in the actual DOM.
This ensures that the browser doesn't have to re-render the entire <div> or <button> elements, saving computation.
Why Virtual DOM Might Be Unnecessary
While the Virtual DOM offers a significant improvement over direct, unoptimized DOM manipulation, frameworks like Svelte and SolidJS argue that it's an abstraction with its own costs and that more direct, compiler-driven or fine-grained approaches can achieve superior results.
Svelte: The Compiler Approach
Svelte takes a fundamentally different approach. It's a compiler that transforms your component code into highly optimized imperative JavaScript during the build step, rather than interpreting it at runtime. This means there's no Virtual DOM at runtime; Svelte components directly manipulate the DOM when state changes.
Let's look at a Svelte example for the same counter:
<!-- Counter.svelte --> <script> let count = 0; function increment() { count += 1; } </script> <div> <p>Count: {count}</p> <button on:click={increment}>Increment</button> </div>
When Svelte compiles this component, it essentially generates JavaScript code that looks something like this (simplified):
// Generated Svelte output (conceptual) function create_fragment(ctx) { let p, t0, t1, t2, button; return { c() { // create elements p = element('p'); t0 = text('Count: '); t1 = text(/*count*/ ctx[0]); t2 = space(); button = element('button'); button.textContent = 'Increment'; listener(button, 'click', /*increment*/ ctx[1]); }, m(target, anchor) { // mount elements insert(target, p, anchor); append(p, t0); append(p, t1); insert(target, t2, anchor); insert(target, button, anchor); }, p(ctx, [dirty]) { // update elements if (dirty & /*count*/ 1) { // if count has changed set_data(t1, /*count*/ ctx[0]); // directly update text node } }, d(detaching) { // destroy elements if (detaching) { detach(p); detach(t2); detach(button); } del_listener(button, 'click', /*increment*/ ctx[1]); } }; }
When count changes, Svelte's generated code directly updates the specific text node (t1) without needing to generate a whole new tree, diff it, and then apply patches. The "diffing" happens at compile time because Svelte already knows exactly which parts of the DOM depend on which state variables. This eliminates the runtime overhead of Virtual DOM reconciliation.
SolidJS: Fine-Grained Reactivity without a Virtual DOM
SolidJS takes inspiration from reactive programming paradigms, similar to Knockout.js or MobX, but applies them to JSX. It compiles JSX templates into actual DOM nodes and then wraps state variables in "signals." When a signal changes, SolidJS precisely knows which parts of the DOM (or other computations) depend on that signal and updates only those specific parts. It avoids a Virtual DOM entirely by creating direct, one-way data bindings from reactive primitives to the DOM elements.
Consider the counter again in SolidJS:
// SolidJS import { createSignal } from 'solid-js'; function Counter() { const [count, setCount] = createSignal(0); return ( <div> <p>Count: {count()}</p> {/* count() reads the signal's value */} <button onClick={() => setCount(count() + 1)}>Increment</button> </div> ); }
When setCount(count() + 1) is called, the count signal's value updates. SolidJS knows that the count() call within the <p> tag's text content depends on this signal. It then directly updates only that text node in the DOM. Similar to Svelte, there's no intermediate Virtual DOM tree. SolidJS's compilation step for JSX effectively creates a series of DOM operations that are triggered directly by signal changes. This "fine-grained reactivity" means only the absolutely necessary updates occur, leading to extremely efficient DOM manipulation.
The key distinction is that while React/Vue use a Virtual DOM to minimize actual DOM updates, Svelte and SolidJS go a step further by eliminating the Virtual DOM altogether. Svelte achieves this through compile-time optimization, generating JavaScript that directly manipulates the DOM. SolidJS achieves this through a reactive graph that directly maps state changes to specific DOM nodes, also compiled down to direct DOM operations.
Conclusion
The Virtual DOM was a brilliant solution to the challenges of inefficient DOM manipulation, revolutionizing frontend development. However, as frameworks evolve, we're seeing compelling alternatives emerge. Svelte and SolidJS demonstrate that by shifting work from runtime to compile-time (Svelte) or by establishing highly optimized, fine-grained reactive connections (SolidJS), the overhead of a Virtual DOM and its reconciliation process can be bypassed entirely. This leads to smaller bundle sizes, faster runtime performance, and often, a simpler mental model for developers. These frameworks highlight that while the Virtual DOM served us well, it is not the only path, nor necessarily the ultimate path, to building fast and efficient web applications. The future of frontend frameworks may very well lie in ever more direct and optimized ways of interacting with the browser's native capabilities.