Pular para o conteúdo
< samuelsantana.dev />
Voltar para o Blog

RxJS vs. Signals: O impacto na performance e na arquitetura de aplicações Angular

Samuel Santana
Publicado em 14 de julho de 2026
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?

  1. Fricção cognitiva e Boilerplate: Um simples cálculo matemático exige combineLatest, map, takeUntil e Subject.
  2. Timing e Glitches: O combineLatest só 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.
  3. Overhead de Memória: Cada async pipe 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 price e quantity forem atualizados no mesmo micro-task, o total() (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 Subscriptions sendo 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 usuarioLogado ou temaAtivo.
  • 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, ou delay. 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?

  1. Separação de responsabilidades explícita: Onde há estado (armazenamento), usamos Signals. Onde há ação/reação (comportamento assíncrono), usamos RxJS.
  2. Adeus Async Pipe: O toSignal gerencia a inscrição (subscription) automaticamente de acordo com o ciclo de vida do componente. Acabou o vazamento de memória.
  3. 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.