Adiós Zone.js: cómo Angular 21 cambió la detección de cambios para siempre
Cuando leí «Angular sin Zone.js» por primera vez pensé que era un truco de optimización para casos extremos. Ahora es el estándar. Y una vez que lo entendés, no querés volver. La detección de cambios deja de ser magia negra para convertirse en algo predecible y controlable.
Qué hacía Zone.js y por qué era problemático
Zone.js es una librería que parcha todas las APIs asíncronas del browser: setTimeout, setInterval, Promise, addEventListener, XMLHttpRequest. Cuando cualquiera de esas operaciones completa, Zone.js notifica a Angular para que ejecute la detección de cambios en todo el árbol de componentes.
El problema: esa detección recorre todo el árbol aunque solo cambiara un campo en un componente profundo. En aplicaciones grandes, eso genera renders innecesarios que degradan el INP (Interaction to Next Paint), la métrica Core Web Vital que Google usa para el ranking.
// Con Zone.js: cualquier click, timeout o promesa dispara detección en TODO el árbol
// ❌ Angular evalúa todos los componentes aunque nada haya cambiado
// Con Signals: solo los componentes que dependen del Signal actualizado se re-renderizan
// ✅ Detección quirúrgica y predecible
Activar Zoneless en Angular 21
// app.config.ts
import {
ApplicationConfig,
provideZonelessChangeDetection
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
provideRouter(routes)
]
};
// angular.json — eliminar zone.js de los polyfills
{
"projects": {
"mi-app": {
"architect": {
"build": {
"options": {
"polyfills": []
}
}
}
}
}
}
// package.json — desinstalar
// npm uninstall zone.js
Signals: el mecanismo que reemplaza a Zone.js
import { Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-contador',
template: `
<p>Contador: {{ contador() }}</p>
<p>Doble: {{ doble() }}</p>
<button (click)="incrementar()">+1</button>
`
})
export class ContadorComponent {
contador = signal(0);
doble = computed(() => this.contador() * 2);
constructor() {
effect(() => {
console.log(`Contador cambió a: ${this.contador()}`);
});
}
incrementar() {
this.contador.update(v => v + 1);
// Solo re-renderiza este componente — no el árbol completo
}
}
Signals vs Observables: cuándo uso cada uno
| Situación | Signal | Observable (RxJS) |
|---|---|---|
| Estado local del componente | ✅ Ideal | Innecesariamente complejo |
| Estado derivado (computed) | ✅ computed() | pipe + map |
| Streams de eventos complejos | No aplica bien | ✅ Ideal |
| HTTP requests | ✅ httpResource | HttpClient + async pipe |
| WebSockets / SSE | Combinado con toSignal() | ✅ Ideal |
| Estado global (store) | ✅ signals + services | NgRx / Akita |
toSignal() y toObservable(): el puente entre mundos
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { debounceTime, switchMap } from 'rxjs/operators';
@Component({ ... })
export class MiComponent {
private http = inject(HttpClient);
// Observable → Signal (sin async pipe en el template)
usuarios = toSignal(
this.http.get<Usuario[]>('/api/usuarios'),
{ initialValue: [] }
);
// Signal → Observable (para operadores RxJS)
busqueda = signal('');
resultados$ = toObservable(this.busqueda).pipe(
debounceTime(300),
switchMap(q => this.http.get(`/api/buscar?q=${q}`))
);
}
El impacto real en rendimiento
En una aplicación de gestión interna que migramos — dashboard con tablas de datos, filtros y actualizaciones frecuentes — la migración a Zoneless redujo el INP de 380ms a 95ms. La diferencia viene de eliminar los ciclos de detección de cambios innecesarios que Zone.js disparaba con cada interacción del usuario, aunque ningún dato de la vista hubiera cambiado.
← Artículo anterior: Angular 21: el cambio de paradigma que no podés ignorar | Serie Angular 20 → 21.2 | Próximo: Signal Forms: cuando los formularios reactivos finalmente tienen sentido →

Dejar un comentario
¿Quieres unirte a la conversación?Siéntete libre de contribuir!