Cómo evitar memory leaks en aplicaciones Angular

frontend-memory-leaks-angular

Frontend Memory Leaks y como evitarlo en Angular

Tiempo estimado de lectura: 3 min

  • Identifica suscripciones, listeners y timers que sobreviven a componentes.
  • Prefiere async pipe y herramientas integradas (takeUntilDestroyed) para evitar fugas.
  • Diagnostica con Chrome DevTools: heap snapshots y comparación de instancias.
  • Disciplina de equipo: code review, linters y políticas de invalidación en singletons.

Introducción

¿Tu SPA se vuelve lenta sin error en consola? Frontend Memory Leaks y como evitarlo en Angular debería ser parte de tu checklist antes de enviar a producción. Si no controlas referencias, suscripciones y listeners, la app “come” memoria hasta que la pestaña muere.

Resumen rápido (lectores con prisa)

Qué es: Objetos no referenciados siguen vivos y el Garbage Collector no los elimina.

Cuándo usar: Siempre que tu SPA cree y destruya componentes, gestiona suscripciones, listeners y timers.

Por qué importa: Fugas causan degradación silenciosa de rendimiento en sesiones largas y móviles.

Cómo funciona: Evita referencias persistentes: async pipe, takeUntilDestroyed, Renderer2 y limpieza en ngOnDestroy.

Frontend Memory Leaks y como evitarlo en Angular: qué pasa y por qué importa

Un memory leak ocurre cuando objetos que ya no necesitas siguen referenciados y el Garbage Collector no los elimina. En Angular esto duele más: la app vive sin recargas, los componentes se crean y destruyen, y cualquier referencia residual se acumula en el heap.

No es magia ni culpa del navegador: es disciplina de código. Y sí, pasa en producción, en móviles y en equipos con pestañas cientos abiertas.

Fuentes útiles:

Vectores comunes y cómo cerrarlos

Suscripciones RxJS sin gestionar

  • Qué pasa: .subscribe() vive más que el componente. El callback mantiene la instancia viva.
  • Soluciones: Async pipe (templates), takeUntilDestroyed() en Angular 16+, o takeUntil con un destroy$ en versiones anteriores.
  • Docs: RxJS takeUntil ; Angular rxjs-interop

Event listeners globales

Qué pasa: window.addEventListener('resize', fn) sin removeEventListener mantiene la referencia.

Solución: usar Renderer2.listen() (retorna un unlisten) o guardar la referencia y llamarla en ngOnDestroy().

Timers (setInterval, setTimeout)

Qué pasa: un timer que cierra sobre this impide la recolección.

Solución: almacenar el id y clearInterval/clearTimeout en ngOnDestroy().

Servicios singleton con datos retenidos

Qué pasa: providedIn: 'root' vive toda la sesión; si almacenas grandes estructuras sin limpieza, la memoria crece.

Solución: políticas explícitas de invalidación / límites / weak references lógicas.

Patrones prácticos (código)

Async pipe — la opción más simple

<!-- template -->
<div *ngIf="data$ | async as data">{{ data.title }}</div>

El async se suscribe y se desuscribe automáticamente cuando el componente muere.

takeUntilDestroyed() — Angular 16+ (recomendado para TS)

import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

this.service.stream()
  .pipe(takeUntilDestroyed())
  .subscribe(x => this.handle(x));

Patrón clásico (Angular <16)

private destroy$ = new Subject<void>();

ngOnInit() {
  this.service.stream()
    .pipe(takeUntil(this.destroy$))
    .subscribe(...);
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

Listeners con Renderer2

private unlisten: () => void;

ngOnInit() {
  this.unlisten = this.renderer.listen('window', 'scroll', this.onScroll.bind(this));
}

ngOnDestroy() {
  this.unlisten?.();
}

Timers

private intervalId?: ReturnType<typeof setInterval>;

ngOnInit() {
  this.intervalId = setInterval(() => this.poll(), 5000);
}

ngOnDestroy() {
  clearInterval(this.intervalId);
}

Cómo diagnosticar fugas — proceso práctico

  1. Abre Chrome DevTools → Memory (Chrome DevTools Memory).
  2. Toma Heap Snapshot.
  3. Navega al componente sospechoso, repite la interacción que crees filtra memoria.
  4. Regresa y toma otro snapshot.
  5. Compara: si ves instancias de tu componente o Detached DOM nodes, hay fuga.

Allocation timeline te muestra crecimiento en tiempo real: una línea que solo sube y no baja es una mala señal.

Política de equipo: lo que realmente previene fugas

Esto no es sólo técnica, es disciplina:

  • Regla: preferir Async Pipe cuando sea posible.
  • Regla: toda suscripción en clase debe documentarse: ¿se cancela? ¿por quién?
  • Code reviews: busca .subscribe( sin takeUntil / takeUntilDestroyed o sin async.
  • Linters y tareas CI: detecta patrones peligrosos (por ejemplo .subscribe( sin unsubscribe en componentes).
  • Tests de rendimiento en CI: integra un paso de e2e que simule navegación repetida y monitorice memoria.

Cierre: no cures síntomas, cambia hábitos

Un memory leak no es un bug aislado; es deuda técnica que crece silenciosamente. Angular te da herramientas —async pipe, takeUntilDestroyed, Renderer2— pero depende del equipo usarlas consistentemente.

Si quieres, en Dominicode podemos publicar una checklist de code-review y un snippet de ESLint que detecte suscripciones peligrosas. No te prometo magia: te prometo menos pestañas explotando. Apúntate y seguimos.

FAQ

¿Qué es un memory leak?

Un memory leak ocurre cuando objetos que ya no necesitas siguen referenciados y el Garbage Collector no los elimina. En una SPA esto provoca acumulación de memoria en el heap mientras la sesión continúa.

¿Cómo detectarlo en Angular?

Usa Chrome DevTools → Memory: toma heap snapshots antes y después de interacciones repetidas. Busca instancias de componentes o Detached DOM nodes que no desaparecen.

¿Cómo manejar suscripciones RxJS?

Prefiere async en templates, o en código use takeUntilDestroyed() (Angular 16+) o takeUntil con un destroy$ y limpieza en ngOnDestroy().

¿Y los listeners globales?

No uses addEventListener sin remover. Usa Renderer2.listen() y llama al “unlisten” en ngOnDestroy().

¿Qué hacer con timers e intervalos?

Guarda los ids retornados por setInterval/setTimeout y llama a clearInterval/clearTimeout en ngOnDestroy().

¿Los servicios singleton pueden filtrar memoria?

Sí. Los servicios providedIn: 'root' viven toda la sesión; si almacenan grandes estructuras sin limpieza la memoria crece. Define políticas de invalidación y límites.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *