Code & DevelopmentCode & Development · 20 Jun 2026
The end of the Virtual DOM: why SolidJS Signals ensure O(1) performance without re-renders
Discover how SolidJS fine-grained reactivity and Signals are changing the front-end by eliminating the Virtual DOM and the cost of re-renders.
The Rise of Signals in Front-end
The Virtual DOM sacrifices performance by re-rendering entire components to compare changes in the element tree.
SolidJS uses fine-grained reactivity, where each signal is connected directly to the DOM node, eliminating the need for diffing.
Components in SolidJS are executed only once, drastically reducing unnecessary processing during updates.
The Signals system eliminates the need for manual dependency arrays, avoiding common bugs related to closures and stale states.
The Signals-based architecture offers update efficiency close to O(1), surpassing the complexity of stores and observables.
There is a strange truth about the last decade of front-end development: we have dedicated our most refined technical intelligence to making a hack faster.
The hack was the Virtual DOM. When your data changes, React rebuilds a copy of your interface in memory, compares it with the previous copy, finds the difference, and applies the adjustment to the actual screen. It is ingenious, and React made it fast. But all this dance exists to answer a question that another architecture does not even need to ask: what changed?
Ryan Carniato asked the better question. What if the code simply knew what changed the instant it changed, and updated only that point? He spent a decade building the answer, named it SolidJS, and, in 2026, the rest of the industry quietly admitted he was right. Signals — the idea at the heart of Solid — are entering JavaScript itself.
This is the story of how “not comparing nodes” went from heresy to standard.
The Virtual DOM fallacy
The Virtual DOM made a reasonable bet. It re-renders the entire component on every change, compares the new tree with the old one, and applies only what differs. In exchange, you get a clean mental model: your interface is a function of your state, and you never touch the DOM manually. This model is a big part of why React won, and it deserves the credit.
But the bet has a cost. To find what changed, the engine first needs to re-check everything that did not change. On a busy screen at sixty frames per second, this means burning cycles re-rendering and re-comparing nodes that were never going to move.
React is super fast here — but if you send the entire page again and again, you have already lost.
Solid made the opposite bet, called fine-grained reactivity. Instead of rebuilding a tree and comparing it, Solid connects each piece of state directly to the single DOM node it controls. Change the state, and that node updates. There is no tree to traverse, no diff to calculate, and no re-rendering.
import { createSignal } from "solid-js";
function Counter() {
const [count, setCount] = createSignal(0);
// Este componente roda UMA vez. Só o nó de texto se atualiza.
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}Read the comment again, because it is the entire revolution in one line: the component runs once. React calls your component function again on every render. Solid calls it a single time, sets up the wiring, and from then on, the signals do the work.
A brief history of reactivity
It helps to see where this fits in. Reactivity in the front-end has gone through three eras, and each one traded one pain for another.
Three eras of reactivity
Each era solved the problem of the previous one and created its own. Signals were the first to refuse the trade-off.
Callbacks
2005–2012
- Rápido e direto — mas o “callback hell” crescia junto com a aplicação.
Virtual DOM
2013–2018
- Previsível e declarativo — mas pesado em memória e CPU pelo diffing constante.
Signals
2019–today
- Atualizações finas em tempo quase constante — previsível E rápido.
The first era was fast and tangled. The second untangled the code and paid in memory. Signals are the first model to refuse the trade-off — you get the declarative clarity of the Virtual DOM era without needing to re-render the world for it.
The anatomy of a signal
Reduce Solid to the bone and you find three primitives. Learn these three and you have learned the framework.
A Signal is the source of state. You read it, you write it — and, this is the part that matters, it tracks on its own who reads it. No dependency arrays.
A Memo (computed) is a derived value. It is lazy and pure: it recalculates only when something it depends on changes, and only when someone actually reads it.
A Effect is the side effect. It is the only place with permission to touch the outside world — the DOM, the console, the network — and it re-executes only when its inputs change.
Signal, Computed and Effect
The three primitives of a signal-first architecture. The component runs only once.
Source of state
Signal
- Rastreamento automático de quem lê, sem arrays de dependência.
Derived value
Computed
- Memoização pura e preguiçosa (lazy)
- só recalcula quando lido e quando muda.
Side effect
Effect
- Atualização cirúrgica direta no DOM, apenas quando as entradas mudam.
import { createSignal, createMemo, createEffect } from "solid-js";
const [preco, setPreco] = createSignal(100); // Signal: a origem
const comImposto = createMemo(() => preco() * 1.1); // Memo: derivado, lazy
createEffect(() => {
console.log("Total:", comImposto()); // Effect: o efeito colateral
});
// Sem arrays de dependência. O Solid rastreia o que você leu.That last comment is the silent superpower, and it deserves its own section.
The end of the dependency array
If you write React, you know the tax. The useEffect and the useMemo force you to list dependencies by hand:
In React, hooks like useEffect and useMemo require you to manually list all dependencies in an array. Forgetting an item can cause hard-to-detect bugs, such as stale closures, missed updates, or renders that do not happen.
This problem is common because React cannot automatically identify which values were used inside the hook, making manual control error-prone.
// React: você mantém o array de dependências na mão
useEffect(() => {
sync(count, query);
}, [count, query]); // esqueça um item e nasce um valor velho ou um update perdidoForget an item and you introduce a bug — a stale closure, a missed update, a render that never fires. It is one of the most common bugs in modern React, and it exists because the framework cannot see which values you actually used.
Solid can. Its effects observe what you read while they run, and the array disappears:
// Solid: o runtime detecta o que foi usado
createEffect(() => {
sync(count(), query()); // sem array, sem bug
});Hold that thought. Soon it will stop being a Solid feature and become a JavaScript feature.
The physical cost
Performance arguments are easy to dismiss, so look at the mechanism, not the marketing. The question is how the cost of an update grows as your application grows.
Architecture | Update Cost | Complexity | Need for Manual Cleanup |
|---|---|---|---|
Store (NgRx) | O(n) | Work proportional to the number of subscribers | Yes |
Observables (RxJS) | O(n²) | Can grow quadratically with long chains | Yes, to avoid leaks |
Signals (SolidJS) | O(1) | Update propagates only to exact computations | No |
With a centralized store (the NgRx pattern), an update tends to cost about O(n) — work proportional to the number of subscribers. With long chains of observables (the RxJS pattern), it can rise to O(n²), and each subscription is something you need to remember to terminate, or it leaks. With signals, an update stays close to O(1) — the change propagates to the exact computations that read it, and nothing else.
The duel of architectures
How the cost of an update grows — and who needs manual cleanup.
~O(n)
Store (NgRx)
- Memória cresce de forma linear. Subscrições exigem limpeza manual.
~O(n²)
Observables (RxJS)
- “Memory churn” de cadeias aninhadas. Subscrições exigem limpeza manual.
~O(1)
Signals
- Memória plana ou sublinear. Limpeza automática
- zero vazamentos esquecidos.
The practical result is the kind of unglamorous victory that the user actually feels: fewer frame drops, less GPU usage, and no slow leaks from forgotten subscriptions. Signals avoid the “memory churn” that chains of nested observables create, and they clean themselves up when their owner is destroyed.
An ecosystem ready for production
None of this matters if the framework is a weekend project, so be clear about its size. Solid passes 1.5 million weekly downloads and leads the State of JS satisfaction surveys year after year. Its creator, Ryan Carniato, is now a principal open source engineer at Netlify and has been refining this same reactive model for over a decade. This longevity is rare, and it shows: the core of Solid is a philosophy, not a fad.
The surrounding stack is complete:
The architectural foundation
From community-maintained libraries to an isomorphic meta-framework.
Meta-framework
SolidStart
- Isomórfico
- file-based routing, server functions, SSR nativo, HTML streaming e dados sobre a Fetch API.
Reactive utilities
Solid Primitives
- De createGeolocation e createIntersectionObserver a debounce e throttle.
Headless UI
Kobalte & Corvu
- Componentes acessíveis (WAI-ARIA) que gerenciam foco e teclado. Você traz o seu CSS, zero lock-in.
Surgical validation
Modular Forms
- Valida um campo em tempo real sem re-renderizar o resto do formulário.
Beyond Signals: the asynchronous chapter
This is where most analyses age poorly. They treat signals as the finish line. Carniato treats them as chapter one.
Signals solved synchronous reactivity — the moment-to-moment flow of state to the screen. The next hard problem is asynchronous reactivity: data arriving over time, loading states, race conditions, the whole fetch mess. SolidJS 2.0, in beta throughout 2026, makes async a first-class citizen. A computation can return a Promise, and the reactive graph handles suspension and resumption on its own. You hand a promise directly to a memo. Suspense has been rebuilt to manage only initial readiness, and batching is now deterministic.
// Solid 2.0: async é cidadão de primeira classe
const user = createAsync(() => getUser(props.id));
return <h1>{user()?.name}</h1>;
// O grafo reativo cuida da suspensão e da retomada por você.This is the part that the “signals are over” crowd does not see. The model that started by tracking a number is now learning to track time.
The war ends in the language
Now step back and see where everyone ended up.
Vue’s Vapor Mode removes the Virtual DOM. Svelte 5 swapped compiler magic for runes — explicit reactivity, looking like a signal. Angular rebuilt its change detection on signals and made zoneless the way forward. Three different syntaxes, all orbiting the same idea that Solid delivered years earlier.
But the real news is not that they copied in parallel. It is that they are collaborating to standardize. The maintainers of Angular, Vue, Svelte, Solid, Preact, Qwik, MobX, Ember, RxJS, and more are working together on a TC39 proposal to add signals to JavaScript itself. It has reached Stage 1 and is modeled on purpose after the Promises/A+ effort, which aligned the ecosystem before TC39 standardized Promises in ES2015. A polyfill already exists.
// A proposta do TC39: signals na linguagem, sem framework
const count = new Signal.State(0);
const double = new Signal.Computed(() => count.get() * 2);
count.set(1);
double.get(); // 2The runtime detects which values a computation reads, so the dependency array — the thing we just complained about — disappears for everyone, in every framework, at the same time. What ended the reactivity war was not one framework winning. It was the standard.
The argument that won
A decade ago, “not comparing the DOM” sounded like a maneuver — a clever trick for a niche library with passionate followers. Today it is the standard direction of the entire field, and it is being written into the language upon which every framework is built.
Solid did not just deliver a fast interface library. It argued, patiently and for ten years, that the Virtual DOM was a detour, and that the screen should update exactly what changed and nothing else. One person held that line long enough for the rest of the industry to arrive at the same place. The argument won.
So, the next time a framework tells you it got faster, ask the better question — the one Carniato asked first. Faster at what? If the answer is “faster at re-checking the parts that never changed,” you already know there was another path.
Written in June 2026. The versions and proposals cited — SolidJS 2.0 Beta's first-class async, SolidStart, and the TC39 Signals proposal (Stage 1) — reflect the state of the front-end ecosystem at that date. Performance comparisons describe the architectural behavior of each model, not a specific benchmark.