Todos los artículos
ARTÍCULO

Signals en Angular 17: adiós al Zone.js, hola al rendimiento real

Guía práctica de Angular Signals: cómo migrar desde RxJS, cuándo usar computed vs effect, y por qué esto cambia fundamentalmente cómo pensamos la reactividad.

Signals en Angular 17: adiós al Zone.js, hola al rendimiento real

Angular Signals llegó para quedarse. Después de dos años de RxJS puro y algunos meses conviviendo con ambos modelos en proyectos reales, puedo decirte con claridad cuándo usar cada uno, cuáles son las trampas comunes y por qué Signals no es solo “otra forma de hacer lo mismo”.

El problema que Signals resuelve

Zone.js, el mecanismo que Angular usaba para detectar cambios, funciona interceptando todas las operaciones asíncronas del navegador (setTimeouts, Promises, eventos DOM). Es brillante, pero tiene un costo: cualquier evento en cualquier parte de tu app puede disparar detección de cambios en toda la jerarquía de componentes.

Signals cambia el modelo: en lugar de detectar qué cambió, tú declaras explícitamente qué depende de qué. El resultado es detección de cambios quirúrgica, sin barrido de árbol.

Los tres primitivos que necesitas conocer

import { signal, computed, effect } from '@angular/core';

// signal: estado mutable
const count = signal(0);
count.set(1);
count.update((v) => v + 1);
console.log(count()); // 2 (se lee como función)

// computed: derivación memoizada (solo recalcula si sus dependencias cambian)
const doubled = computed(() => count() * 2);
console.log(doubled()); // 4

// effect: side effect reactivo (cuidado con el abuso)
effect(() => {
	console.log(`El contador cambió a: ${count()}`);
});

computed es memoizado automáticamente. Si count() no cambia, doubled() devuelve el valor cacheado sin ejecutar la función. Esto es detección de cambios O(1) por derivación.

Signals en componentes: el patrón correcto

@Component({
	selector: 'app-users',
	changeDetection: ChangeDetectionStrategy.OnPush,
	template: `
		<div *ngFor="let user of users()">{{ user.name }}</div>
		<p>Total: {{ userCount() }}</p>
	`,
})
export class UsersComponent {
	private userService = inject(UserService);

	// Signal de estado local
	readonly searchTerm = signal('');

	// Señal derivada del servicio + estado local
	readonly users = computed(() =>
		this.userService
			.allUsers()
			.filter((u) =>
				u.name.toLowerCase().includes(this.searchTerm().toLowerCase()),
			),
	);

	readonly userCount = computed(() => this.users().length);

	updateSearch(term: string) {
		this.searchTerm.set(term);
	}
}

Nota el ChangeDetectionStrategy.OnPush. Con Signals, el componente solo se re-renderiza cuando un Signal que usa en el template cambia. No hay barrido de árbol. No hay Zone.js involucrado.

Cuándo seguir usando RxJS

Signals no reemplaza RxJS. Los casos donde RxJS sigue siendo la herramienta correcta:

  • Streams de eventos: teclas presionadas, clicks con debounce
  • Operaciones asíncronas con cancelación: requests que se cancelan al navegar
  • Combinación compleja de fuentes: combineLatest, switchMap, race

La buena noticia: puedes convertir entre ambos mundos fácilmente:

import { toSignal, toObservable } from '@angular/core/rxjs-interop';

// Observable → Signal
const search$ = fromEvent(input, 'input').pipe(
	debounceTime(300),
	map((e) => (e.target as HTMLInputElement).value),
);
const searchSignal = toSignal(search$, { initialValue: '' });

// Signal → Observable
const searchObs$ = toObservable(this.searchTerm);

El error más común que veo

Usar effect para sincronizar signals entre sí. Es el anti-patrón más frecuente:

// ❌ MAL: effect para derivar estado
effect(() => {
  this.filteredUsers.set(
    this.users().filter(u => u.active === this.showActive())
  );
});

// ✅ BIEN: computed para derivar estado
readonly filteredUsers = computed(() =>
  this.users().filter(u => u.active === this.showActive())
);

effect es para side effects con el mundo exterior (DOM directo, logs, analytics). Para derivar estado, siempre computed.

El veredicto después de 6 meses

Signals simplifica dramáticamente el modelo mental de reactividad en Angular. El código es más legible, más predecible y más performante. Si estás en Angular 16+, es el momento de empezar a migrar.