Signals em JavaScript: Por Que Todo Framework Está Adotando (E O Que Isso Significa pro React)
Algo incomum tá acontecendo no mundo frontend. Angular adotou Signals. Svelte substituiu seu modelo de reatividade pelos Runes. Solid.js foi construído em cima deles desde o primeiro dia. O sistema de reatividade do Vue sempre foi parecido com signals por baixo dos panos. Qwik, Preact, e até frameworks legado como Ember — todos convergindo pra mesma primitiva.
Enquanto isso, o React — o framework que domina o mercado — tá indo deliberadamente na direção oposta, apostando tudo num compilador pra resolver os problemas de performance que os signals resolvem por design.
A proposta TC39 Signals, que já está avançando no processo de padronização do JavaScript, quer incorporar essa primitiva reativa diretamente na linguagem. Se der certo, signals não vão ser só funcionalidade de framework — vão ser parte do JavaScript, como Promise ou Array.
Essa é a mudança arquitetural mais significativa no desenvolvimento frontend desde que o Virtual DOM foi introduzido há mais de uma década. Bora entender o que tá rolando de verdade, por que importa, e o que significa pro código que você escreve hoje.
O Que São Signals, Afinal?
Se a gente tirar as APIs específicas de cada framework, um signal é um conceito enganosamente simples: um contêiner reativo pra um valor que notifica automaticamente seus dependentes quando muda.
Pense como uma variável observável com rastreamento automático de dependências. Quando você lê um signal dentro de uma computação, o runtime lembra dessa dependência. Quando o valor do signal muda, só as computações que realmente dependem dele são re-executadas.
// Criar um valor reativo const count = signal(0); // Computação derivada — rastreia dependências automaticamente const doubled = computed(() => count.value * 2); // Efeito colateral — re-executa quando as dependências mudam effect(() => { console.log(`Count: ${count.value}, doubled: ${doubled.value}`); }); // Só as computações que dependem de `count` re-executam count.value = 5; // Console: "Count: 5, doubled: 10"
Não tem array de dependências. Não tem gerenciamento manual de inscrições. Não tem algoritmo de diffing. O runtime sabe exatamente o que depende de quê porque observou o grafo de dependências em tempo de execução.
As Três Primitivas
Toda implementação de signals, independente do framework, é construída sobre três primitivas:
1. Signal (Estado) — Um contêiner reativo que mantém um único valor.
const name = signal("Alice"); console.log(name.value); // "Alice" name.value = "Bob"; // Notifica os dependentes
2. Computed (Estado Derivado) — Um valor derivado de um ou mais signals. É lazy: só recalcula quando lido, e só se uma dependência mudou.
const firstName = signal("Alice"); const lastName = signal("Smith"); const fullName = computed(() => `${firstName.value} ${lastName.value}`); // fullName não recalcula até você ler E uma dependência ter mudado
3. Effect (Efeitos Colaterais) — Uma função que executa quando suas dependências rastreadas mudam. É aqui que você faz updates de DOM, requests de rede ou logging.
effect(() => { document.title = fullName.value; // Re-executa só quando fullName muda });
Esse modelo de três primitivas é a fundação. Agora bora ver quão fundo vai essa toca do coelho.
Como Signals Funcionam Por Dentro
A mágica dos signals não tá na API — tá no rastreamento automático de dependências. Bora construir um runtime simplificado de signals do zero pra entender a mecânica interna.
O Grafo de Dependências
No seu núcleo, um runtime de signals mantém um grafo acíclico direcionado (DAG) de dependências:
┌─────────┐ ┌─────────┐
│ signal A │────▶│computed C│────▶ effect E
└─────────┘ └─────────┘
┌─────────┐ ▲
│ signal B │────────┘
└─────────┘
Quando Signal A muda, o runtime percorre o grafo e só re-executa Computed C e Effect E. Os outros dependentes de Signal B (se existirem) ficam intocados.
Uma Implementação Mínima
Aqui vai uma implementação funcional de signals em ~50 linhas de JavaScript:
let currentObserver = null; function signal(initialValue) { let value = initialValue; const subscribers = new Set(); return { get value() { // Rastrear: se alguém tá observando, registrar esse signal if (currentObserver) { subscribers.add(currentObserver); } return value; }, set value(newValue) { if (newValue === value) return; // Pular se não mudou value = newValue; // Notificar: re-executar todos os inscritos for (const subscriber of subscribers) { subscriber(); } } }; } function computed(fn) { let cachedValue; let dirty = true; const computation = () => { dirty = true; }; return { get value() { if (dirty) { const prevObserver = currentObserver; currentObserver = computation; cachedValue = fn(); currentObserver = prevObserver; dirty = false; } return cachedValue; } }; } function effect(fn) { const execute = () => { const prevObserver = currentObserver; currentObserver = execute; fn(); currentObserver = prevObserver; }; execute(); // Executar imediatamente pra estabelecer dependências }
A técnica chave é a pilha global de observers (currentObserver). Quando um computed ou effect executa, ele se estabelece como o observer atual. Qualquer signal lido durante essa execução adiciona automaticamente o observer ao seu conjunto de inscritos. Por isso você nunca precisa declarar dependências manualmente.
Otimizações de Produção
Implementações reais adicionam várias otimizações críticas:
1. Avaliação Push-Pull — Em vez de re-executar todos os inscritos quando um signal muda, implementações modernas marcam computações downstream como "dirty" (push) e só recalculam quando o valor é lido (pull).
2. Execução Livre de Glitches — Se Signal A e Signal B mudam na mesma microtask, um computed que depende de ambos deve executar uma vez só, não duas.
const a = signal(1); const b = signal(2); const sum = computed(() => a.value + b.value); batch(() => { a.value = 10; b.value = 20; }); // sum só executa uma vez com (10, 20)
3. Limpeza Automática — Quando um effect re-executa, suas inscrições antigas são limpas automaticamente. Tchau memory leaks.
4. Checks de Igualdade — Signals pulam notificações se o novo valor é idêntico ao anterior (usando Object.is por padrão).
A Proposta TC39 Signals
A parada mais empolgante não tá acontecendo dentro de nenhum framework — tá acontecendo no nível da linguagem. A proposta TC39 Signals quer padronizar a primitiva reativa diretamente no JavaScript.
Por Que Padronizar?
Cada framework tem sua própria implementação de signals. Angular Signals não interoperam com createSignal do Solid. Uma biblioteca de date picker feita com Preact Signals não funciona num app Vue sem wrappers.
A proposta TC39 resolve isso com uma API padrão Signal sobre a qual todos os frameworks podem construir:
// API da Proposta TC39 (Stage 1, sujeito a mudanças) const counter = new Signal.State(0); const isEven = new Signal.Computed(() => (counter.get() & 1) === 0); // Frameworks empacotam isso com suas próprias APIs ergonômicas // mas o grafo reativo subjacente é compartilhado
O Que a Proposta Fornece
A proposta foca no algoritmo do grafo reativo, não na camada de renderização:
Signal.State— Valor reativo de leitura/escritaSignal.Computed— Valor reativo derivado (lazy, cacheado)Signal.subtle.Watcher— API de baixo nível pra integração com frameworks
Ponto importante: a proposta não inclui effect(). Efeitos são específicos do framework — como você atualiza o DOM, agenda renders ou agrupa mudanças fica na mão de cada framework. O padrão só fornece as primitivas do grafo reativo.
A Visão de Interoperabilidade
Imagina um futuro onde:
- Uma biblioteca de gráficos usa
Signal.Stateinternamente - Você usa ela num app Angular com Angular Signals (construído sobre
Signal.State) - Seu colega usa a mesma biblioteca num app Solid
- A reatividade flui sem problemas porque o grafo é compartilhado
Esse é o sonho — reatividade de estado compartilhada em todo o ecossistema JavaScript.
O Panorama dos Frameworks: Quem Usa Signals e Como
Angular Signals (v17+)
A adoção de signals pelo Angular foi um evento sísmico. O framework famoso por RxJS, Zones e change detection reescreveu seu modelo de reatividade:
import { signal, computed, effect } from '@angular/core'; @Component({ template: ` <h1>{{ fullName() }}</h1> <button (click)="updateName()">Mudar Nome</button> ` }) export class UserComponent { firstName = signal('Alice'); lastName = signal('Smith'); fullName = computed(() => `${this.firstName()} ${this.lastName()}`); logger = effect(() => { console.log(`Nome mudou pra: ${this.fullName()}`); }); updateName() { this.firstName.set('Bob'); } }
A mudança arquitetural chave: Angular agora pode pular Zone.js completamente pra change detection. Em vez de fazer dirty-checking da árvore inteira de componentes em cada evento, Angular só atualiza os nós DOM específicos vinculados a signals que mudaram. Resultado: renderização 30-50% mais rápida em benchmarks reais.
Solid.js — Signals Desde o Dia Um
Solid.js provou que signals podiam alimentar um framework UI de nível produção:
import { createSignal, createMemo, createEffect } from "solid-js"; function Counter() { const [count, setCount] = createSignal(0); const doubled = createMemo(() => count() * 2); createEffect(() => { console.log(`Count: ${count()}, Doubled: ${doubled()}`); }); return ( <button onClick={() => setCount(c => c + 1)}> {count()} × 2 = {doubled()} </button> ); }
A diferença crítica do React: Solid não re-executa componentes. A função Counter executa exatamente uma vez. O JSX é compilado em instruções de atualização DOM granulares. Quando count muda, só os nós de texto que mostram count() e doubled() são atualizados — a função do componente nunca re-executa.
Zero diffing de Virtual DOM, zero re-renders, zero necessidade de memoização. Nunca.
Svelte Runes (v5)
Svelte 5 substituiu sua reatividade anterior (a sintaxe de label $:) por Runes — essencialmente signals em tempo de compilação:
<script> let count = $state(0); let doubled = $derived(count * 2); $effect(() => { console.log(`Count: ${count}, Doubled: ${doubled}`); }); </script> <button onclick={() => count++}> {count} × 2 = {doubled} </button>
A elegância dos Runes é que parecem variáveis normais. O compilador transforma $state, $derived e $effect em primitivas signal internas, mas a experiência do desenvolvedor parece escrever JavaScript normal.
A Reatividade do Vue (Composition API)
Vue usa um sistema de reatividade parecido com signals desde a Composition API do Vue 3:
<script setup> import { ref, computed, watchEffect } from 'vue'; const count = ref(0); const doubled = computed(() => count.value * 2); watchEffect(() => { console.log(`Count: ${count.value}, Doubled: ${doubled.value}`); }); </script> <template> <button @click="count++"> {{ count }} × 2 = {{ doubled }} </button> </template>
O ref do Vue é Signal. computed é Computed. watchEffect é Effect. Os nomes são diferentes, mas o grafo reativo subjacente é arquitetonicamente idêntico. Vue já fazia signals antes de signals virarem tendência.
Preact Signals
Preact tomou uma abordagem única adicionando signals como biblioteca companion que se integra com o Virtual DOM:
import { signal, computed } from "@preact/signals"; const count = signal(0); const doubled = computed(() => count.value * 2); function Counter() { return ( <button onClick={() => count.value++}> {count} × 2 = {doubled} </button> ); }
O diferentão do Preact Signals: você pode passar um signal direto pro JSX ({count} em vez de {count.value}), e o Preact inscreve no nível DOM, pulando o diff do Virtual DOM pra aquele nó. Performance próxima do Solid mantendo o modelo familiar de componentes tipo React.
O Caminho Divergente do React: Hooks + Compilador vs. Signals
Aqui é onde a coisa fica controversa. Todos os frameworks principais estão convergindo em signals, mas o React — o mais usado — escolheu explicitamente não adotar. Entender o porquê revela diferenças filosóficas fundamentais.
O Argumento do React Contra Signals
1. Fluxo de Dados Top-Down — React é projetado sobre a ideia de que componentes são funções de seus props e estado. Signals quebram esse modelo porque atualizam nós DOM diretamente, sem passar pela função do componente.
2. Facilidade de Debug — No modelo do React, você pode colocar um breakpoint em qualquer componente e ver o render completo em cada mudança de estado. Com signals, updates acontecem de forma granular e não tem "ciclo de render" pra inspecionar.
3. A Aposta no Compilador — A posição do React é que o compilador pode entregar performance nível signals sem mudar o modelo de programação.
Os Contra-Argumentos
1. Overhead Fundamental — Mesmo com memoização perfeita, React ainda re-executa funções de componente e faz diff de árvores Virtual DOM. Signals pulam ambos os passos.
React (com compilador):
Mudança de estado → Re-executar função → Diff vDOM → Patch DOM
Signals:
Mudança de estado → Patch DOM diretamente
2. Custo de Runtime — A memoização do React Compiler adiciona overhead de runtime (buscas em cache, checks de igualdade). Signals só trabalham quando valores realmente mudam.
3. As "Regras do React" — O compilador exige que o código siga as "Regras do React" (componentes puros, sem mutações durante render). Violações causam bugs de corretude silenciosos. Signals não têm essas restrições.
4. Fragmentação do Ecossistema — Se TC39 padronizar signals, todos os frameworks exceto React compartilharão uma primitiva reativa comum.
Comparação de Performance
Números aproximados do js-framework-benchmark baseados em resultados públicos pra uma tabela de 10.000 linhas:
| Operação | React 19 + Compiler | Solid.js (Signals) | Angular (Signals) | Svelte 5 (Runes) |
|---|---|---|---|---|
| Criar 10k linhas | ~420ms | ~190ms | ~230ms | ~200ms |
| Atualizar cada 10 linhas | ~80ms | ~18ms | ~25ms | ~20ms |
| Trocar duas linhas | ~45ms | ~12ms | ~15ms | ~14ms |
| Selecionar linha | ~8ms | ~2ms | ~3ms | ~2ms |
| Remover linha | ~38ms | ~6ms | ~9ms | ~7ms |
| Memória (pós-criação) | ~9 MB | ~4 MB | ~4.5 MB | ~3.5 MB |
Nota: Esses são valores aproximados baseados em tendências de benchmarks públicos. Os números reais variam por hardware, navegador e versão do framework. Confira js-framework-benchmark pra resultados atualizados.
O padrão é consistente: frameworks baseados em signals superam a abordagem do compilador React por aproximadamente 2-4x na maioria das operações. A diferença de memória é ainda mais dramática porque signals não mantêm uma árvore Virtual DOM.
Migração Prática: Adicionando Signals ao Seu Stack
Se Você Tá no Angular
Já tá lá. Angular 17+ signals estão prontos pra produção. Começa a migrar dos padrões pesados em RxJS:
// Antes: RxJS Observables @Component({...}) export class UserComponent implements OnInit, OnDestroy { user$!: Observable<User>; private destroy$ = new Subject<void>(); ngOnInit() { this.user$ = this.userService.getUser().pipe( takeUntil(this.destroy$) ); } ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } } // Depois: Angular Signals + resource API @Component({...}) export class UserComponent { userId = input.required<string>(); user = resource({ request: () => this.userId(), loader: ({ request: id }) => this.userService.getUser(id) }); }
Sem gerenciar inscrições. Sem padrões takeUntil. Sem cleanup no OnDestroy.
Se Você Tá no React (Usando Preact Signals)
Dá pra usar signals no React hoje com @preact/signals-react:
import { signal, computed } from "@preact/signals-react"; const count = signal(0); const doubled = computed(() => count.value * 2); function Counter() { return ( <div> <p>{count.value} × 2 = {doubled.value}</p> <button onClick={() => count.value++}>Incrementar</button> </div> ); }
Pera: é uma integração de terceiros. Funciona se conectando ao ciclo de render do React, então você não pega os benefícios completos de performance de signals nativos. Mas pega a ergonomia do rastreamento automático de dependências sem useMemo/useCallback manual.
Se Tá Começando do Zero
Se você tá escolhendo um framework pra um projeto novo em 2026 e performance é crítica:
- Solid.js — Máxima performance, menor bundle, experiência signals mais "pura"
- Svelte 5 — Melhor experiência de desenvolvimento (Runes parecem JS normal), excelente performance
- Angular — Melhor escolha pra times enterprise grandes (TypeScript nativo, tooling completo)
- Vue — Ótimo equilíbrio entre performance e maturidade do ecossistema
Exemplo Real: Validação Reativa de Formulários
Bora construir algo prático pra ver signals em ação.
Vanilla Signals (Estilo Proposta TC39)
const email = new Signal.State(""); const password = new Signal.State(""); const emailError = new Signal.Computed(() => { const value = email.get(); if (!value) return "Email é obrigatório"; if (!value.includes("@")) return "Formato de email inválido"; return null; }); const passwordStrength = new Signal.Computed(() => { const value = password.get(); if (value.length === 0) return "empty"; if (value.length < 8) return "weak"; if (/(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%])/.test(value)) return "strong"; return "medium"; }); const isFormValid = new Signal.Computed(() => { return emailError.get() === null && passwordStrength.get() !== "weak" && passwordStrength.get() !== "empty"; });
Implementação com Solid.js
import { createSignal, createMemo, Show } from "solid-js"; function SignupForm() { const [email, setEmail] = createSignal(""); const [password, setPassword] = createSignal(""); const emailError = createMemo(() => { const value = email(); if (!value) return "Email é obrigatório"; if (!value.includes("@")) return "Formato de email inválido"; return null; }); const passwordStrength = createMemo(() => { const value = password(); if (value.length === 0) return "empty"; if (value.length < 8) return "weak"; if (/(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%])/.test(value)) return "strong"; return "medium"; }); const isValid = createMemo(() => emailError() === null && !["weak", "empty"].includes(passwordStrength()) ); return ( <form> <input type="email" value={email()} onInput={(e) => setEmail(e.target.value)} classList={{ error: !!emailError() }} /> <Show when={emailError()}> <span class="error">{emailError()}</span> </Show> <input type="password" value={password()} onInput={(e) => setPassword(e.target.value)} /> <div class={`strength-${passwordStrength()}`}> Força: {passwordStrength()} </div> <button disabled={!isValid()}>Cadastrar</button> </form> ); }
Quando o usuário digita no campo de email:
- Só o signal
emailmuda - Só
emailErrorrecalcula (nãopasswordStrength) - Só
isValidrecalcula - Só os nós DOM vinculados a
emailError()eisValid()atualizam
O input de senha, indicador de força e todos os outros nós DOM ficam completamente intocados. No React, toda a função do componente re-executaria, todas as expressões JSX seriam re-avaliadas, e o React faria diff da subárvore Virtual DOM completa.
Pra Onde o Frontend Tá Indo
A convergência pra signals não é coincidência. Várias forças tão atuando ao mesmo tempo:
1. O Teto de Performance do Virtual DOM
Diffing de Virtual DOM foi uma ideia brilhante em 2013 quando os motores JavaScript eram lentos. Em 2026, os motores são extraordinariamente rápidos, e o overhead de "criar árvore virtual → diff → patch" virou o gargalo.
Signals eliminam o intermediário. Mudança de estado → Update DOM. Sem etapa de diffing.
2. A Ascensão da Arquitetura Islands
Astro, Qwik, e até Next.js (com RSC) tão se movendo pra "ilhas" de interatividade num mar de HTML estático. Signals encaixam naturalmente porque cada ilha pode ter seu próprio grafo reativo local sem afetar o resto da página.
3. O Endgame do TC39
Se Signal.State e Signal.Computed virarem parte do padrão JavaScript:
- Todos os frameworks construídos sobre eles interoperam automaticamente
- Motores do navegador podem otimizar o grafo reativo em nível nativo
- Bibliotecas de terceiros usam uma primitiva reativa universal
O fim da era do "vendor lock-in" pra gerenciamento de estado.
Conclusão
O mundo frontend tá passando por uma revolução de reatividade. Signals provaram ser um modelo fundamentalmente mais eficiente pra gerenciamento de estado UI do que a abordagem de diffing de Virtual DOM que o React popularizou.
Todos os frameworks principais exceto React adotaram signals. A proposta TC39 tá trabalhando pra padronizá-los no próprio JavaScript. Angular viu melhorias de renderização de 30-50% pós-migração. Solid.js entrega 2-4x a performance do React em benchmarks padronizados.
A aposta do React no compilador é ousada e pode diminuir a diferença, mas não pode eliminar o overhead arquitetural fundamental de re-executar componentes e fazer diff de árvores virtuais. Se o React eventualmente adotar signals, ou conseguir provar que a abordagem do compilador é superior, a competição tá fazendo todo o ecossistema ficar melhor.
Independente de qual framework você usa hoje, entender signals não é mais opcional — é conhecimento fundamental. O grafo reativo é o futuro do gerenciamento de estado frontend, e já tá aqui.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit