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/setter
  • effect(): executa uma função sempre que suas dependências mudam
  • computed(): 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