Hey! Let's dig into one of the most exciting debates in frontend engineering right now — Virtual DOM vs Signals.
For over a decade, React's Virtual DOM was the undisputed champion. But now Solid.js, Svelte, Angular, and even Preact are moving to signals. Is the Virtual DOM dead? Let's find out.
How the Virtual DOM Works
React's approach:
- State changes → React re-renders the entire component and its children
- React builds a new Virtual DOM tree (a plain JS object representation of the UI)
- React diffs the new tree against the previous one
- Only the actual DOM changes are applied (the "reconciliation" step)
function Counter() {
const [count, setCount] = useState(0);
// When count changes, this ENTIRE function re-runs
// React re-creates the VDOM for this component
// Then diffs it against the previous VDOM
return (
<div>
<h1>Count: {count}</h1> {/* Only this text changes */}
<p>Some static content</p> {/* This is diffed too */}
<HeavyComponent /> {/* This re-renders too! */}
</div>
);
}Why Virtual DOM Was Brilliant (in 2013)
Before React, we were doing direct DOM manipulation with jQuery. The problem? Keeping the DOM in sync with application state was a nightmare. Forget to update one element, and your UI is out of sync.
React's insight: just re-render everything when state changes, and we'll figure out the minimal DOM updates. Developers stopped thinking about DOM updates and just described what the UI should look like.
This was a massive productivity win. The Virtual DOM was fast enough, and developer experience was incredible.
The Overhead Problem
But "fast enough" has limits. The Virtual DOM does unnecessary work:
- Re-rendering entire subtrees: When
countchanges,HeavyComponentre-renders even though it doesn't usecount - Diffing static content: React diffs nodes that literally never change
- Object allocation: Every render creates new VDOM objects that are immediately garbage collected
- The memo tax: To avoid unnecessary re-renders, you need
React.memo,useMemo,useCallback— all of which add complexity
// The "memo tax" — performance optimization that adds complexity
const MemoizedChild = React.memo(({ data }) => {
return <ExpensiveList items={data} />;
});
function Parent() {
const [count, setCount] = useState(0);
const data = useMemo(() => processData(rawData), [rawData]);
const handleClick = useCallback(() => setCount(c => c + 1), []);
return (
<div>
<button onClick={handleClick}>{count}</button>
<MemoizedChild data={data} />
</div>
);
}This is a lot of ceremony just to tell React "hey, this thing didn't change, don't bother re-rendering it."
Enter Signals — Fine-Grained Reactivity
Signals take the opposite approach. Instead of re-running entire components, they track exactly which pieces of UI depend on which pieces of state and update only those.
A signal is a reactive primitive — a container for a value that notifies its subscribers when it changes:
// Solid.js
import { createSignal } from 'solid-js';
function Counter() {
const [count, setCount] = createSignal(0);
// This function runs ONCE to set up the DOM
// It does NOT re-run when count changes
console.log('Component setup'); // Logged once!
return (
<div>
<h1>Count: {count()}</h1> {/* Only this text node updates */}
<p>Some static content</p> {/* Never touched again */}
<HeavyComponent /> {/* Never re-runs */}
</div>
);
}Notice the difference? In Solid, the component function runs once. When count changes, only the text node {count()} updates. No diffing. No re-rendering. No memo tax.
How Solid.js Pulls This Off
Solid uses reactive tracking at compile time. When it sees {count()} in the JSX, the compiler wraps it in an effect that subscribes to the count signal. When count changes, only that specific effect re-runs and updates that specific DOM node.
// What the Solid compiler generates (simplified)
const h1 = document.createElement('h1');
createEffect(() => {
h1.textContent = `Count: ${count()}`; // Only this runs on update
});No Virtual DOM. No diffing. Direct DOM updates. Surgically precise.
Svelte — Compile-Time Reactivity
Svelte takes yet another approach — it moves reactivity to compile time:
<script>
let count = 0; // Svelte knows this is reactive
</script>
<h1>Count: {count}</h1>
<button on:click={() => count++}>Increment</button>The Svelte compiler analyzes your code, figures out which DOM nodes depend on which variables, and generates imperative update code:
// What Svelte compiles to (simplified)
if (changed.count) {
set_text(h1, `Count: ${count}`);
}No runtime reactivity system needed. No Virtual DOM. Just surgical DOM updates generated at build time.
Angular's New Signals API
Angular jumped on the signals train too:
@Component({
template: `<h1>Count: {{ count() }}</h1>`
})
class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(c => c + 1);
}
}Angular is moving away from Zone.js (which detected changes by monkey-patching every async API) toward signals for more predictable, fine-grained change detection.
React's Response — The React Compiler
React isn't standing still. The React Compiler (formerly React Forget) automatically adds memoization to your components:
// What you write
function Counter({ data }) {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<ExpensiveList items={data} />
</div>
);
}
// The compiler automatically memoizes ExpensiveList
// No need for React.memo or useMemoReact's bet: keep the Virtual DOM and the familiar programming model, but let the compiler eliminate the overhead. You don't need to learn signals — the compiler handles optimization transparently.
Which Approach Wins?
Signals win on:
- Raw performance (no diffing overhead)
- Memory efficiency (no VDOM object creation)
- Predictable updates (you know exactly what re-renders)
Virtual DOM wins on:
- Developer experience (just re-render, don't think about subscriptions)
- Ecosystem maturity (React's ecosystem is massive)
- Flexibility (server components, concurrent features, Suspense)
The honest answer: For most apps, the performance difference doesn't matter. React's Virtual DOM is fast enough for 95% of use cases. Signals shine in performance-critical scenarios — heavy animations, real-time dashboards, massive lists.
Pick based on your team's needs and ecosystem requirements, not benchmark numbers.
See you next time!