Signals em JavaScript: reatividade fina sem Virtual DOM explicada com código
1. O que são Signals e por que eles emergiram agora?
Signals são uma primitiva reativa que permite rastrear dependências em nível granular — literalmente no nível de cada propriedade ou valor. Diferente de abordagens que re-renderizam componentes inteiros, Signals atualizam apenas as partes do DOM que realmente mudaram.
Historicamente, a reatividade evoluiu dos watchers do AngularJS (2010), passando pelo Virtual DOM do React (2013), até as soluções mais finas como Vue 3 (2020) com ref() e reactive(). Em 2024/2025, Signals ganharam tração massiva com frameworks como SolidJS, Qwik e Preact Signals, que demonstraram que é possível obter performance de framework compilado sem abrir mão da DX de runtime.
A razão principal para essa emergência é o cansaço com o custo do Virtual DOM em aplicações complexas — especialmente em dispositivos móveis e cenários com alta frequência de atualizações.
2. Anatomia de um Signal: Estrutura e Funcionamento Interno
Um sistema de Signals possui três componentes essenciais:
signal(): cria um valor reativo com getter/settereffect(): executa uma função sempre que suas dependências mudamcomputed(): deriva um novo valor reativo a partir de outros Signals
O ciclo de vida é: criação → leitura (registra subscriber) → atualização (notifica subscribers) → reexecução dos effects.
// Implementação manual de um Signal simples
function createSignal(initialValue) {
let value = initialValue;
const subscribers = new Set();
function read() {
if (currentEffect) {
subscribers.add(currentEffect);
}
return value;
}
function write(newValue) {
if (value !== newValue) {
value = newValue;
subscribers.forEach(fn => fn());
}
}
return [read, write];
}
let currentEffect = null;
function createEffect(fn) {
currentEffect = fn;
fn();
currentEffect = null;
}
3. Signals vs Virtual DOM: Diferenças Fundamentais de Arquitetura
O Virtual DOM (React) funciona em três etapas: renderizar componente → diff da árvore virtual → aplicar patches no DOM real. Isso significa que mesmo uma mudança minúscula em um campo de input pode re-renderizar todo o formulário.
Signals, por outro lado, mantêm um grafo de dependências que conecta diretamente o estado ao DOM. Quando um Signal muda, apenas os nós do DOM que dependem dele são atualizados — sem diffing, sem reconciliação.
// Virtual DOM: re-renderiza todo o componente
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c+1)}>{count}</button>;
}
// Signals: atualiza apenas o texto no botão
const [count, setCount] = createSignal(0);
<button onClick={() => setCount(c+1)}>{count()}</button>
Em cenários como listas com 10.000 itens, formulários com 50 campos ou animações a 60fps, Signals podem ser 10x-100x mais rápidos que Virtual DOM, pois eliminam o overhead de diffing.
4. Implementação Prática: Criando um Sistema de Reatividade com Signals
Vamos construir um sistema completo de reatividade:
// Sistema de reatividade completo
const context = [];
function createSignal(value) {
const subscribers = new Set();
function read() {
const running = context[context.length - 1];
if (running) {
subscribers.add(running);
running.dependencies.add(subscribers);
}
return value;
}
function write(nextValue) {
if (value === nextValue) return;
value = nextValue;
const toRun = new Set(subscribers);
toRun.forEach(fn => fn.execute());
}
return [read, write];
}
function createEffect(fn) {
const effect = {
execute() {
effect.dependencies.forEach(dep => dep.delete(effect));
effect.dependencies = new Set();
context.push(effect);
fn();
context.pop();
},
dependencies: new Set()
};
effect.execute();
return () => effect.dependencies.forEach(dep => dep.delete(effect));
}
function createComputed(fn) {
const [get, set] = createSignal();
createEffect(() => set(fn()));
return get;
}
Exemplo prático — contador reativo:
const [count, setCount] = createSignal(0);
const [double, setDouble] = createSignal(0);
createEffect(() => {
setDouble(count() * 2);
document.getElementById('display').textContent = count();
});
// Uso: setCount(5) atualiza display e double automaticamente
5. Signals em Frameworks Modernos: SolidJS, Preact e Qwik
SolidJS usa Signals como base fundamental. Seu compilador transforma JSX em chamadas diretas ao DOM, sem Virtual DOM:
// SolidJS
const [count, setCount] = createSignal(0);
return <h1>{count()}</h1>; // Compilado para: h1.textContent = count()
Preact Signals oferecem integração leve com React e Preact. Você pode usar Signals dentro de hooks:
// Preact Signals com React
const count = signal(0);
function Counter() {
return <button onClick={() => count.value++}>{count.value}</button>;
}
Qwik combina Signals com resumability — a capacidade de retomar a aplicação sem reexecutar todo o código de hidratação. Signals permitem que apenas as partes relevantes sejam ativadas.
6. Padrões Avançados e Boas Práticas com Signals
Composição de Signals com computed:
const [todos, setTodos] = createSignal([]);
const [filter, setFilter] = createSignal('all');
const filteredTodos = createComputed(() => {
const f = filter();
return todos().filter(t => f === 'all' || t.status === f);
});
Tratamento de efeitos colaterais com cleanup:
createEffect(() => {
const id = setInterval(() => console.log('tick'), 1000);
return () => clearInterval(id); // cleanup automático
});
Armadilhas comuns:
- Loops infinitos: nunca atualizar um Signal dentro de um effect que depende dele
- Dependências não rastreadas: acessar Signals fora de effects não registra subscribers
- Estado compartilhado: Signals mutáveis podem causar efeitos colaterais imprevisíveis
7. Comparação com Alternativas: RxJS, Zustand, Jotai e Context API
RxJS é mais poderoso para streams complexos (debounce, throttle, combinação), mas tem curva de aprendizado alta. Signals são mais simples e adequados para estado síncrono.
Zustand e Jotai usam conceitos similares de estado atômico, mas sem o grafo de dependências automático dos Signals. Jotai se aproxima mais, usando atoms que se comportam como Signals.
Context API do React causa re-renderizações em todos os consumidores, mesmo que apenas uma propriedade mude. Signals resolvem isso com dependências finas.
8. O Futuro da Reatividade: Signals como Padrão Web?
A proposta TC39 para Signals nativos (Stage 1, 2024) sugere que Signals podem se tornar parte do JavaScript padrão. Isso permitiria que frameworks compartilhassem uma primitiva comum de reatividade.
Implicações:
- Frameworks poderiam interoperar via Signals
- Menos código duplicado entre bibliotecas
- Ferramentas de debugging nativas no navegador
Porém, desafios permanecem: legado de aplicações baseadas em Virtual DOM, ecossistema fragmentado e debugging ainda imaturo.
Adotar Signals hoje é ideal para novos projetos que exigem alta performance, especialmente em dashboards, editores de código, jogos e aplicações mobile-web.
Referências
- TC39 Proposal: Signals — Proposta oficial para adicionar Signals ao ECMAScript, com especificação detalhada e exemplos
- SolidJS Documentation: Reactivity — Tutorial oficial da SolidJS sobre createSignal, createEffect e createMemo
- Preact Signals Guide — Documentação oficial do Preact sobre integração de Signals com componentes
- Understanding Signals in JavaScript (Mozilla Hacks) — Artigo técnico da Mozilla explicando o funcionamento interno de Signals
- Qwik Documentation: Signals and Resumability — Como Qwik usa Signals para reatividade sem hidratação completa
- React vs Signals: Performance Comparison (LogRocket) — Benchmark prático comparando Virtual DOM com Signals em cenários reais