Domina la detección de cambios en Angular
Si tu aplicación Angular se ralentiza sin razón aparente, probablemente es porque no entendés cómo funciona la detección de cambios (change detection). Durante años, Zone.js hizo toda la “magia” por vos, pero con las Signals de Angular 16+ eso cambió: ahora tenés control granular sobre qué se recalcula y cuándo. La diferencia entre una app que corre a 60 FPS y otra que freezea constantemente muchas veces está en cómo configures change detection.
En 30 segundos
- Zone.js intercepta todas las operaciones asincrónicas (eventos, promesas, timers, HTTP) y obliga a Angular a recorrer todo el árbol de componentes buscando cambios.
- OnPush (desde Angular 2) permite indicar que un componente solo se actualiza si sus @Input cambian de referencia, lo que salta subtrees completos y ahorra CPU.
- Angular Signals (v16+) reemplazan el dirty checking con reactividad granular: solo se actualiza la UI que “escucha” la señal que cambió.
- Angular 20 en Developer Preview incluye soporte “zoneless” — sin Zone.js, solo Signals — mejor rendimiento y menor overhead.
- El problema: la mayoría de los equipos sigue usando la estrategia default de Zone.js, y paga un costo brutal en rendimiento en aplicaciones complejas.
Angular Change Detection es el mecanismo que determina cuándo la UI debe actualizar en respuesta a cambios de datos. Por defecto, Angular usa Zone.js para interceptar operaciones asincrónicas (clicks, timers, promesas HTTP), y cuando alguna termina, recorre el árbol completo de componentes comparando cada valor del template con el que tenía antes. Si algo cambió, actualiza el DOM. El problema: en aplicaciones grandes, este recorrido total es ineficiente.
La Magia de Zone.js: Cómo Funciona por Defecto
Zone.js es una librería que hace monkey-patching de casi todo en JavaScript. Intercepta eventos DOM, timers, promesas, llamadas HTTP — básicamente, cualquier operación que pueda cambiar datos. Cuando tu aplicación boots, Angular corre dentro de una “zona” especial. El momento en que cualquiera de esas operaciones termina, Zone.js le avisa a Angular: “Ey, algo pasó afuera. Probablemente tengas que chequear.”
Angular entonces hace algo que suena simple pero es brutal en aplicaciones grandes: recorre el árbol de componentes desde la raíz hacia abajo. Por cada componente, compara cada expresión del template contra el valor anterior. Según explica la documentación oficial de Angular, este proceso se llama “dirty checking” — básicamente, pregunta “¿algo se ensució?” a todo el árbol, sin saber dónde está realmente el cambio.
El insight clave: Angular no observa tu data. Observa las operaciones asincrónicas, porque eso es lo que típicamente cambia data (un usuario hace click, una API responde, un timer dispara). Cada una de esas operaciones es una oportunidad de que algo haya mutado, así que mejor chequear todo.
Funciona. Es automático. Y para aplicaciones pequeñas, está bien. (Spoiler: para aplicaciones grandes, es un desastre.)
El Costo de la Magia: Por Qué Zone.js No Escala
Imaginá que tenés un dashboard con 200 componentes. Un usuario tipea en un input, se dispara un evento, Zone.js avisa a Angular, y Angular… recorre 200 componentes comparando cada valor. ¿Cuántos de esos 200 realmente cambiaron? Uno. Tal vez dos. Pero Angular no lo sabe. Tiene que preguntar a todos.
En CPU-bound tasks (operaciones pesadas), esto se nota como jank: freezeos visuales, animaciones que stutteran, scroll que trastabilla. El navegador intenta mantener 60 FPS (16ms por frame), pero tu script de Angular tardó 25ms en recorrer el árbol. Frame perdido. Mala experiencia. Sobre eso hablamos en pruebas automatizadas para componentes.
El problema es arquitectónico: Zone.js es de “cambios globales”. No sabe dónde ocurrió el cambio. Angular tampoco. Así que lo más seguro es chequear todo. Máquina. Automático. Ineficiente.
OnPush Change Detection: Control Explícito del Rendimiento
Angular 2 trajo ChangeDetectionStrategy.OnPush, una alternativa: “Che, Angular. Solo me verificas a mí si mis @Input cambian de referencia, o si ocurre un evento en mi componente, o si me lo pedís explícitamente.” Es un opt-in, no la default.
Con OnPush, Angular es perezoso. Si un componente padre tiene OnPush y sus @Input no cambiaron de referencia, Angular salta el componente entero y todos sus hijos. No recorre. No compara. Siguiente rama del árbol.
Ojo: “cambio de referencia” significa que si mutas un objeto (cambias una propiedad sin asignar un objeto nuevo), OnPush no lo detecta. Si tenías:
this.user.name = "Ariel";
Con OnPush, el template no se actualiza. Tenés que hacer:
this.user = { ...this.user, name: "Ariel" };
Nueva referencia. Mismo objeto (casi). OnPush detecta.
El trade-off: menos CPU, pero más disciplina. Tenés que pensar en immutability. Para equipos con experiencia en functional programming o React, OnPush es natural. Para el resto, es una trampa esperando a que cometas mutaciones silenciosas.
La Revolución de Signals: Reactividad Granular
Angular 16 trajo Signals, y cambió el juego completamente. En lugar de “chequear todo el árbol cuando algo async pasa”, Signals dicen: “Yo trackeo exactamente qué componentes leen qué datos. Cuando cambio, aviso solo a los que me leen.”
Es reactividad granular. Fino. Preciso. Sin dirty checking.
Crear una señal es trivial:
const count = signal(0);
Cuando alguien en el template usa {{ count() }}, Angular registra que ese template depende de esa señal. Cambias el valor con count.set(1), y solo ese template (y sus dependientes directos) se invalida. No todo el árbol. Solo eso. Relacionado: cómo otros frameworks manejan cambios.
Computed es para valores derivados. Imaginá que tenés price y quantity. Querés un total:
const total = computed(() => price() * quantity());
Total se actualiza automáticamente cuando price o quantity cambian. Y si total nunca se leyó, computed ni se ejecuta. Lazy.
Effect es para efectos secundarios. Cuando una señal cambia, querés logguear, guardar a BD, lo que sea:
effect(() => { console.log("Count:", count()); });
Cada vez que count cambia, el effect se ejecuta. Sin suscripciones manuales. Sin leak de memoria. Automático.
Computed vs Effect: Cuándo Usar Cada Una
Computed es derivación: datos que dependen de otras señales. Si lees una computed, obtenés el valor. Nunca hace efectos secundarios (idealmente).
Effect es para operaciones impuras: guardar a local storage, logguear, disparar requests HTTP. Effect se ejecuta automáticamente cuando sus dependencias cambian.
La regla: si es valor puro, usa computed. Si tiene efecto secundario, usa effect. Ya lo cubrimos antes en mejores prácticas de seguridad en desarrollo.
El Futuro Zoneless: Angular 20 sin Zone.js
Angular 20 llegó en 2026 con Developer Preview de “zoneless” — soporte para aplicaciones sin Zone.js. Todo control reactivo va a depender de Signals.
Según reportes de la comunidad, zoneless promete mejor rendimiento, menos overhead, y una arquitectura más predecible. Sin Zone.js haciendo monkey-patching, el navegador tiene menos “ruido” asincrónico que rastrear.
Ahora bien, esto todavía está experimental. No está listo para producción. Pero es el futuro. Angular se está moviendo hacia reactividad granular por defecto, no change detection global.
Tabla Comparativa: Zone.js vs OnPush vs Signals
| Estrategia | Cómo Detecta Cambios | Rendimiento | Complejidad | Uso Ideal |
|---|---|---|---|---|
| Zone.js (Default) | Recorre todo el árbol después de cada async | Bajo en apps pequeñas, crítico en grandes | Ninguna — automático | Prototipos, apps simples |
| OnPush | Solo si @Input cambia de referencia o evento local | Medio-alto (requiere immutability) | Moderada — pensar en refs | Apps medianas, equipos disciplinados |
| Signals | Trackeo granular de dependencias | Alto — solo actualiza lo que cambió | Baja — natural, reactivo | Apps grandes, flujos complejos, futuro |
| Zoneless + Signals | Granular + sin monkey-patching | Muy alto — menos overhead | Baja — limpio | Futuro (Angular 20+) |

Errores Comunes que Sabotean Change Detection
1. Mutar objetos sin cambiar referencia con OnPush activado
Pasaste a OnPush. Bien. Pero tu componente hace esto:
this.user.email = "[email protected]";
El template no se actualiza. OnPush solo detecta cambios de referencia. El objeto es el mismo (aunque internamente cambió). Solución: asignación nueva:
this.user = { ...this.user, email: "[email protected]" };
2. Suscripciones manuales en lugar de async pipe con OnPush
Si hacés una suscripción manual a un Observable:
this.data$ = someService.getData(); this.data$.subscribe(val => this.myValue = val);
Con OnPush, el template puede no actualizarse cuando llega el valor (porque la suscripción es asincrónica y no cambió ningún @Input). Mejor opción:
{{ (data$ | async) }}
El async pipe dispara change detection cuando llega el valor.
3. markForCheck() olvidado después de async
Con OnPush, si tenés un efecto asincrónico que actualiza state (ej, un timer), el template no se recalcula hasta que Angular no lo marque explícitamente. Por eso existe markForCheck():
setTimeout(() => { this.value = 10; this.cdr.markForCheck(); }, 1000); En principios fundamentales de optimización profundizamos sobre esto.
Sin markForCheck(), el template sigue mostrando el valor viejo.
4. Modificar @Input programáticamente sin conocer las reglas
Un componente hijo recibe un @Input. Internamente lo modifica para “normalizarlo”. Con OnPush, eso es invisible para el padre. El padre cree que sigue siendo el valor original, porque desde su punto de vista, nunca cambió la referencia.
Regla: @Input es “entrada”, no “estado interno”. Si necesitás modificarlo, guardalo en una variable local y usa eso.
Qué Está Confirmado / Qué No
- Confirmado: Zone.js hace monkey-patching de APIs asincrónicas (probado en producción desde Angular 2, 2016).
- Confirmado: OnPush reduce el costo de change detection al permitir opt-in (documentado en la guía oficial de Angular).
- Confirmado: Signals están disponibles desde Angular 16 (febrero 2023), y mejoran performance en aplicaciones complejas (benchmarks propios de Google).
- Confirmado: Angular 20 incluye Developer Preview de “zoneless” (anunciado en 2025, llegó en 2026).
- No confirmado: Cuándo Angular hará zoneless la default (no hay fecha oficial aún, probablemente Angular 21-22).
- No confirmado: Si migrar apps legacy a Signals será automático o requerirá refactor manual (evaluando codemods).
Preguntas Frecuentes
¿Debería activar OnPush en todos mis componentes ahora?
Depende. Si tu aplicación corre a 60 FPS sin problemas, no lo necesitás. OnPush agrega complejidad (pensar en immutability, markForCheck). Usalo si hacés profiling (Chrome DevTools → Performance) y ves que change detection es el cuello de botella. En apps grandes, suele serlo.
¿Signals reemplazan a RxJS y Observables?
No completamente. Observables son para streams de valores en el tiempo (data que llega del backend, clicks del usuario, timers). Signals son para estado sincrónico (una variable que reacciona a cambios). Usá Observables para data externa, Signals para UI state. Combinable.
¿Necesito Signals en una aplicación pequeña?
No. Si tu app tiene 20 componentes y cambia lentamente, Signals no te da ventaja. Zone.js funciona. Usá Signals cuando el state es complejo, las actualizaciones son frecuentes, o ves jank.
¿Zoneless es compatible con Observables?
Parcialmente. Zoneless todavía soporta Observables, pero para que funcione, necesitás wrappear la suscripción en un effect. Observables fueron diseñados asumiendo Zone.js. Zoneless los trata de forma diferente.
¿Cuál es la diferencia entre ChangeDetectionStrategy.Default y OnPush?
Default: Angular recorre el árbol completo siempre. OnPush: Angular salta el componente si no cambió su @Input (por referencia). OnPush es más eficiente, pero requiere immutability. Default es más permisivo pero más lento en apps grandes.
Conclusión
Change detection es el corazón de Angular. Durante años, Zone.js hizo la magia automáticamente, pero el costo fue brutal en aplicaciones grandes: recorrer el árbol entero cada vez que algo async pasa es una estrategia de fuerza bruta. OnPush mejoró el panorama permitiendo opt-in, pero requería disciplina (immutability, markForCheck).
Signals llegaron en Angular 16 y cambiaron el juego: reactividad granular sin dirty checking global. Cambias una señal, solo los componentes que la leen se actualizan. Punto. Limpio, eficiente, predecible.
Para 2026, la recomendación es clara: si estás escribiendo código Angular nuevo, considerá Signals como la opción por defecto. Si mantenés una aplicación legacy en Zone.js + change detection default, perfilá primero (Chrome DevTools → Performance tab). Si ves que change detection consume más del 20% del tiempo de frame, activá OnPush en los componentes problemáticos o migrá a Signals.
El futuro es zoneless (Angular 20+ Developer Preview ya está disponible). Ese futuro se construye con Signals. Conviene estar listo.
Fuentes
- Angular Change Detection Explained: Zone.js, OnPush, and the Signal Revolution — Dev.to
- Angular Signals Official Guide — Angular.dev
- Angular 20 y el Futuro sin Zone.js: La Revolución Zoneless — Dev.to
- How Does Angular Change Detection Really Work — Angular University
- Señales en Angular: Cómo Escribir Código Reactivo — FreeCodeCamp






