Code & Development

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 end of the Virtual DOM: why SolidJS Signals ensure O(1) performance without re-renders

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.

Funcionamento do Virtual DOM nova vs antiga aplica diferenças Reconstrução da árvore do componente Re-renderiza componente inteiro a cada mudança Comparação entre árvore nova e antiga Identifica diferenças para atualizar só o necessário Aplicação das mudanças no DOM real Atualiza o DOM real com as diferenças detectadas
How the Virtual DOM works

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.

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.

Fluxo da reatividade fina no SolidJS liga a dispara cria define mantém Estado (Signal) Pedaço de estado individual Ligação direta Estado ligado ao nó do DOM Atualização no nó DOM Só o nó afetado é atualizado Componente monta uma vez Função executada só na montagem Sem re-renderização Não há reexecução da função do componente
Flow of fine-grained reactivity in SolidJS
JSX
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
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.

Each era solved the problem of the previous one and created its own. Signals were the first to refuse the trade-off.

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
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.

The three primitives of a signal-first architecture. The component runs only once.

JSX
import { createSignal, createMemo, createEffect } from "solid-js";const [preco, setPreco] = createSignal(100);      // Signal: a origemconst comImposto = createMemo(() => preco() * 1.1); // Memo: derivado, lazycreateEffect(() => {  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:

JSX
// React: você mantém o array de dependências na mãouseEffect(() => {  sync(count, query);}, [count, query]); // esqueça um item e nasce um valor velho ou um update perdido

Forget 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:

JSX
// Solid: o runtime detecta o que foi usadocreateEffect(() => {  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
~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.

How the cost of an update grows — and who needs manual cleanup.

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
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.

From community-maintained libraries to an isomorphic meta-framework.

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.

JSX
// Solid 2.0: async é cidadão de primeira classeconst 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.

JAVASCRIPT
// A proposta do TC39: signals na linguagem, sem frameworkconst count = new Signal.State(0);const double = new Signal.Computed(() => count.get() * 2);count.set(1);double.get(); // 2

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.