
RxJS vs. Signals: O impacto na performance e na arquitetura de aplicações Angular
Quem constrói aplicações Angular há mais de alguns anos provavelmente desenvolveu uma espécie de Síndrome de Estocolmo com o RxJS. Nós o amamos pela sua robustez em lidar com fluxos complexos de dados assíncronos, mas também sofremos com a curva de aprendizado, o excesso de boilerplate e, principalmente, com os temidos memory leaks causados por Subscriptions esquecidas.
Durante muito tempo, o RxJS foi a resposta do Angular para tudo. Precisava fazer uma requisição HTTP? Observable. Precisava escutar um clique? Observable. Precisava gerenciar um simples estado de "aberto/fechado" de um modal ou um contador? Adivinha? Um BehaviorSubject.
Essa abordagem de usar streams baseadas em eventos para gerenciar o estado síncrono da UI cobrava seu preço na arquitetura e na performance. Mas com a estabilização dos Signals, o Angular mudou as regras do jogo.
Neste artigo, vamos dissecar como essa mudança impacta o código que escrevemos no dia a dia, aprofundar nos ganhos reais de performance e, o mais importante, definir arquiteturalmente quando escolher um ou outro.
O problema do Estado Reativo com RxJS
Para entender o impacto dos Signals, precisamos olhar para as cicatrizes que o RxJS deixou nas nossas bases de código. Imagine um cenário clássico: uma tela de checkout onde você tem um produto, uma quantidade selecionada e precisa calcular o preço total.
Na arquitetura "clássica" do Angular com RxJS, a implementação típica seria algo assim:
import { Component, OnDestroy } from '@angular/core';
import { BehaviorSubject, combineLatest, map, Subject, takeUntil } from 'rxjs';
@Component({
selector: 'app-checkout',
template: `
<div>
<p>Preço unitário: {{ price$ | async | currency }}</p>
<button (click)="increment()">+</button>
<p>Quantidade: {{ quantity$ | async }}</p>
<h2>Total: {{ total$ | async | currency }}</h2>
</div>
`
})
export class CheckoutComponent implements OnDestroy {
private destroy$ = new Subject<void>();
// O estado precisa ser encapsulado em Subjects
price$ = new BehaviorSubject<number>(150);
quantity$ = new BehaviorSubject<number>(1);
// O estado derivado requer operadores e combineLatest
total$ = combineLatest([this.price$, this.quantity$]).pipe(
map(([price, quantity]) => price * quantity),
takeUntil(this.destroy$) // <- O clássico boilerplate de cleanup
);
increment() {
this.quantity$.next(this.quantity$.getValue() + 1);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Qual é a dor arquitetural aqui?
- Fricção cognitiva e Boilerplate: Um simples cálculo matemático exige
combineLatest,map,takeUntileSubject. - Timing e Glitches: O
combineLatestsó emite quando todos os streams internos emitem pelo menos uma vez. Se não tomarmos cuidado com a inicialização, a UI simplesmente não renderiza. - Overhead de Memória: Cada
asyncpipe no template cria uma subscription separada. Cada operador no pipe chain cria uma nova instância de Observable. Em listas longas, isso destrói a performance.
O RxJS foi projetado para coordenar eventos assíncronos ao longo do tempo (como websockets, debounce de inputs). Usá-lo para segurar o valor síncrono da quantidade de um carrinho é matar uma mosca com um canhão.
A Chegada dos Signals: Reatividade Síncrona
Signals resolvem o problema de estado da UI de forma muito mais enxuta. Um Signal é um "recipiente" que encapsula um valor e notifica seus consumidores sempre que esse valor muda.
Diferente de um Observable, um Signal sempre tem um valor atual (é síncrono) e não tem o conceito de "completar" ou "falhar".
Vamos refatorar o mesmo componente utilizando Signals:
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-checkout-signal',
template: `
<div>
<p>Preço unitário: {{ price() | currency }}</p>
<button (click)="increment()">+</button>
<p>Quantidade: {{ quantity() }}</p>
<h2>Total: {{ total() | currency }}</h2>
</div>
`
})
export class CheckoutSignalComponent {
price = signal(150);
quantity = signal(1);
// Computed: recalcula automaticamente APENAS se price ou quantity mudarem
total = computed(() => this.price() * this.quantity());
increment() {
this.quantity.update(q => q + 1);
}
}
Neste modelo, o código volta a parecer TypeScript/JavaScript puro. Sem .getValue(), sem vazamento de memória para gerenciar no ngOnDestroy.
Desempenho Sob o Capô: Por que Signals são mais rápidos?
A mudança na sintaxe é excelente para o desenvolvedor, mas o verdadeiro ganho arquitetural acontece na engine de renderização.
O gargalo do Zone.js
Historicamente, o Angular dependeu do Zone.js. Essa biblioteca intercepta todas as APIs assíncronas do navegador (setTimeout, requisições, eventos de clique) e avisa o Angular: "Algo aconteceu em algum lugar. Verifique se o DOM precisa ser atualizado".
Quando você clica no botão + usando a abordagem clássica, o Angular percorre a árvore de componentes inteira comparando o estado anterior com o novo (dirty checking). É um processo ineficiente, especialmente em aplicações pesadas.
Fine-Grained Reactivity (Reatividade Granular)
Com Signals, entramos na era da reatividade cirúrgica.
O Angular constrói um grafo de dependências reativo. Quando você usa {{ quantity() }} no template, o framework cria um vínculo direto entre aquele nó do DOM e o Signal específico. Se quantity mudar, o Angular não precisa verificar a árvore inteira. Ele sabe exatamente qual pedaço do HTML precisa ser re-renderizado.
Impacto real na performance:
- Zoneless: Signals pavimentaram o caminho para remover o Zone.js (
provideExperimentalZonelessChangeDetection()). Isso reduz o tamanho do bundle inicial e elimina os ciclos de Change Detection desnecessários. - Glitch-Free: Se
priceequantityforem atualizados no mesmo micro-task, ototal()(que é um computed) será recalculado apenas uma vez. No RxJS, multiplas emissões síncronas podem causar renderizações intermediárias indesejadas (glitches na UI). - Menos pressão no Garbage Collector: Sem dezenas de
Subscriptionssendo criadas e destruídas a cada navegação de rota, o consumo de memória cai drasticamente.
Quando escolher um ou outro? (A Matriz de Decisão)
O maior erro que equipes têm cometido ao adotar as versões recentes do Angular é tentar jogar o RxJS no lixo. Signals não substituem o RxJS completamente.
Eles resolvem problemas diferentes. A regra de ouro arquitetural atual é:
Signals gerenciam ESTADO. RxJS gerencia FLUXOS E EVENTOS.
Use Signals quando:
- Estado local do componente: Variáveis simples (booleanos para modais, contadores, inputs de formulário síncronos).
- Valores derivados: Qualquer dado que dependa matematicamente ou logicamente de outro estado (usando
computed). - Estado global simples: Serviços que armazenam dados como
usuarioLogadooutemaAtivo. - Binding no Template: Sempre que possível, a variável que o HTML consome deve ser um Signal.
Use RxJS quando:
- Requisições de Rede (HTTP): O ciclo de vida de um request (pending, success, error, cancelamento) é naturalmente assíncrono e baseado em fluxo.
- Complexidade de Tempo (Timing): Operações que precisam de
debounceTime,throttleTime, oudelay. Signals não possuem conceito de tempo. - Orquestração Assíncrona e Race Conditions: Precisar cancelar a requisição A porque a requisição B começou (
switchMap), ou combinar múltiplas chamadas paralelas (forkJoin). - Eventos baseados em push: Websockets, Server-Sent Events (SSE), e streams de eventos do DOM (mousemove, drag and drop).
Abordagens Arquiteturais: Unindo o Melhor dos Dois Mundos
Em aplicações corporativas, o código mais elegante hoje utiliza a Interoperabilidade entre as duas tecnologias.
Imagine um cenário avançado: uma busca de produtos. Precisamos escutar a digitação do usuário, aplicar um debounce para não derrubar o backend, cancelar requests obsoletos, tratar erros e, no fim, exibir o estado atual na tela de forma otimizada.
A arquitetura moderna faz isso transitando entre Signals e Observables sem atrito:
import { Component, signal, inject } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, distinctUntilChanged, switchMap, catchError, of } from 'rxjs';
@Component({
selector: 'app-product-search',
template: `
<!-- O binding da UI consome e atualiza o Signal diretamente -->
<input (input)="updateSearch($event)" placeholder="Busque um produto..." />
<!-- Consumo limpo e performático na View -->
@if (products(); as results) {
<ul>
@for (product of results; track product.id) {
<li>{{ product.name }}</li>
}
</ul>
}
`
})
export class ProductSearchComponent {
private http = inject(HttpClient);
// 1. ESTADO: Signal guarda o que o usuário digita (Síncrono e simples)
searchTerm = signal<string>('');
// 2. FLUXO: toObservable() leva o valor para o mundo do RxJS
private searchResults$ = toObservable(this.searchTerm).pipe(
debounceTime(300), // Lida com o tempo
distinctUntilChanged(), // Evita redundância
switchMap(term => { // Cancela requests pendentes (Race condition)
if (!term) return of([]);
return this.http.get<any[]>(`/api/products?q=${term}`).pipe(
catchError(() => of([]))
);
})
);
// 3. ESTADO FINAL: toSignal() traz de volta para consumo otimizado no template
products = toSignal(this.searchResults$, { initialValue: [] });
updateSearch(event: Event) {
const input = event.target as HTMLInputElement;
this.searchTerm.set(input.value);
}
}
Por que essa é a abordagem definitiva?
- Separação de responsabilidades explícita: Onde há estado (armazenamento), usamos Signals. Onde há ação/reação (comportamento assíncrono), usamos RxJS.
- Adeus Async Pipe: O
toSignalgerencia a inscrição (subscription) automaticamente de acordo com o ciclo de vida do componente. Acabou o vazamento de memória. - Performance Máxima: A UI é renderizada usando a Reatividade Granular dos Signals, totalmente preparada para o futuro Zoneless do Angular.
Conclusão
A adoção dos Signals no Angular não é apenas um "açúcar sintático". É uma refatoração profunda no motor do framework que corrige problemas históricos de performance (Zone.js) e de ergonomia (excesso de RxJS para tarefas triviais).
Ao invés de ver a mudança como uma ameaça ao conhecimento que você construiu em RxJS, encare como uma ferramenta de refinamento. Deixe o RxJS brilhar naquilo que ele faz de melhor: orquestrar a bagunça do mundo assíncrono. E confie nos Signals para entregar essa informação à interface do usuário da forma mais rápida, limpa e performática possível.
Comentários
Carregando comentários...
Participe da conversa
Entre com sua conta para comentar neste artigo.