O Angular passou cerca de uma década com o mesmo modelo central: NgModules pra conectar as peças, Zone.js pra saber quando checar o DOM em busca de mudanças, e decorators como @Input() em campos comuns da classe. O Angular 17 até o 20 substituiu as três peças — componentes standalone, signals, e input()/output() baseados em função. É tentador colocar isso na gaveta de "sintaxe mais bonita". A história de detecção de mudanças por baixo é um runtime diferente, não só um retoque visual.
Como um componente Angular era escrito por uma década
Aqui está um pequeno componente de contador do jeito que seria escrito no Angular 15, a última versão major antes de qualquer coisa disso começar a existir — baseado em NgModule, orientado a decorators, implicitamente dependente do Zone.js pra disparar a detecção de mudanças:
// counter.component.ts (Angular 15)
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{ count }}</span>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
@Input() count = 0;
@Output() countChange = new EventEmitter<number>();
increment(): void {
this.count++;
this.countChange.emit(this.count);
}
decrement(): void {
this.count--;
this.countChange.emit(this.count);
}
}// counter.module.ts (Angular 15)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CounterComponent } from './counter.component';
@NgModule({
declarations: [CounterComponent],
imports: [CommonModule],
exports: [CounterComponent],
})
export class CounterModule {}Dois arquivos pra renderizar um botão. O próprio componente não tem ideia de como ou quando sua view é atualizada — isso é trabalho do Zone.js, rodando silenciosamente por baixo de tudo.
O mesmo componente no Angular atual
O Angular 20 elimina o módulo por completo (standalone é o padrão desde o Angular 17) e substitui os decorators @Input/@Output pelas funções input() e output() baseadas em signal:
// counter.component.ts (Angular 20)
import { Component, input, output } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
<span>{{ count() }}</span>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
count = input(0);
countChange = output<number>();
increment(): void {
this.countChange.emit(this.count() + 1);
}
decrement(): void {
this.countChange.emit(this.count() - 1);
}
}Um arquivo só. Sem NgModule, sem importar EventEmitter, e count agora é uma função que você chama — count() — em vez de um campo que você lê. Esse último detalhe parece cosmético. Não é.
A parte que não é só sintaxe: detecção de mudanças
A detecção de mudanças do Angular clássico tem um único trabalho: depois de literalmente qualquer evento assíncrono disparar — um clique, um setTimeout, uma resposta HTTP — o Zone.js percebe, e o Angular percorre toda a árvore de componentes checando cada binding pra ver se algo mudou. O Zone.js faz isso fazendo monkey-patch de APIs do navegador (setTimeout, addEventListener, Promise, XMLHttpRequest) pra conseguir se conectar em qualquer coisa que possa ter mudado algum estado. Funciona, mas significa que o custo de um único clique de botão é proporcional ao tamanho do app inteiro, não ao que de fato mudou.
Um signal não é só uma variável reativa — é uma leitura rastreada. Quando um template chama count() dentro de uma interpolação, o Angular registra que esse binding específico depende daquele signal específico. Quando o valor do signal muda, o Angular sabe exatamente quais bindings atualizar, sem percorrer nada que não precisa. É isso que torna possível o Angular zoneless — uma vez que toda leitura reativa passa por signals em vez de acesso comum a campo, não sobra nada pro Zone.js observar:
// main.ts (Angular 20, bootstrap zoneless)
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [provideZonelessChangeDetection()],
});Compare isso com a forma como todo app Angular costumava inicializar, com o Zone.js carregado como uma dependência implícita e não configurável antes de qualquer linha de código da aplicação rodar:
// main.ts (Angular 15)
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
platformBrowserDynamic().bootstrapModule(AppModule);
// zone.js é importado como efeito colateral em polyfills.ts —
// a detecção de mudanças não tem opção de desligar.Remover o Zone.js não é uma otimização de tamanho de bundle. Remove um mecanismo inteiro — monkey-patching global de APIs assíncronas — e substitui por rastreamento de dependência por binding. Isso é um modelo de execução diferente pro framework inteiro, não uma forma diferente de declarar um componente.
RxJS não desaparece, ganha uma fronteira
Código existente baseado em RxJS não precisa ser reescrito. O @angular/core/rxjs-interop fornece toSignal() e toObservable() pra que os dois modelos se encontrem numa fronteira explícita em vez de tudo ser forçado num paradigma só:
import { toSignal } from '@angular/core/rxjs-interop';
import { inject } from '@angular/core';
export class UserProfileComponent {
private userService = inject(UserService);
// A chamada HTTP ainda retorna um Observable; toSignal() lê o valor
// mais recente pro grafo de signals, pra templates e computed()
// poderem depender dele como qualquer outro signal.
user = toSignal(this.userService.getCurrentUser(), {
initialValue: null,
});
}O RxJS continua fazendo o que sempre fez bem — compor streams de eventos assíncronos, dar debounce, tentar de novo, combinar múltiplas fontes. Os signals assumem a parte pra qual o RxJS sempre foi um encaixe um pouco desajeitado: estado síncrono simples de UI que um template lê diretamente.
A lição
É fácil olhar pra input() em vez de @Input() e colocar a atualização inteira na gaveta de limpeza de API. A mudança real é que o Angular substituiu uma varredura global de detecção de mudanças, disparada por APIs assíncronas com monkey-patch e percorrendo a árvore inteira, por um grafo de dependências granular onde cada leitura reativa é rastreada individualmente. A sintaxe nova é um efeito colateral visível disso; não é a atualização em si. Uma base de código que migra os decorators mas continua disparando mudanças de estado através de campos mutáveis comuns fora do grafo de signals ganha a sintaxe sem o benefício real — o modelo de runtime só compensa quando as mudanças de estado de fato fluem pelos signals do início ao fim.
Vamos conversar?
Tem um projeto, uma vaga ou só quer trocar uma ideia sobre o assunto? Envie uma mensagem.
Entrar em contato