DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Memory Leaks in Angular: The Silent Performance Killer

Memory Leaks in Angular: The Silent Performance Killer

Memory Leaks in Angular: The Silent Performance Killer

How to Detect, Prevent, and Eliminate Memory Leaks Before Users Feel Them

Cristian Sifuentes\
4 min read · Feb 24, 2026


Everything works.

The app loads.\
Navigation works.\
Features behave normally.

But after 30 minutes:

  • The app feels slow\
  • CPU usage increases\
  • Memory grows continuously\
  • Tabs start freezing\
  • No error appears

That's a memory leak.

And Angular applications --- even in 2026 --- are not immune.

This article is not about beginner advice.\
It's about lifecycle ownership, architectural discipline, and
production-grade memory hygiene in Angular 21+.


The Bug That Doesn't Show Up Immediately

Memory leaks rarely explode.\
They accumulate.

In enterprise applications:

  • Users keep dashboards open for hours\
  • WebSockets stream constantly\
  • Lists re-render frequently\
  • Lazy routes mount/unmount repeatedly

A small leak multiplied by time becomes system-level degradation.

This is not an Angular problem.

It's a lifecycle boundary problem.


What a Memory Leak Actually Is

Beginner Definition

"You forgot to unsubscribe."

Partially correct. But incomplete.

Real Definition

A memory leak occurs when:

  • A reference is retained beyond its intended lifecycle\
  • A subscription outlives its component\
  • An event listener remains attached\
  • A detached view is still reachable\
  • A closure holds a destroyed component

If something prevents garbage collection, it leaks.

Angular does not magically clean everything.

You must design ownership explicitly.


The Classic Leak (Still Happening in 2026)

ngOnInit() {
  this.api.getData().subscribe(data => {
    this.store.set(data);
  });
}
Enter fullscreen mode Exit fullscreen mode

Looks harmless.

Now imagine:

  • This component mounts/unmounts often\
  • It lives inside lazy-loaded routes\
  • Navigation occurs repeatedly

That subscription survives unless explicitly cleaned.

Each mount creates another active stream.

Memory grows silently.


Modern Angular 21 Solution: takeUntilDestroyed()

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

@Component({ standalone: true })
export class DashboardComponent {

  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.api.getData()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(data => {
        this.store.set(data);
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Now:

  • Subscription auto-terminates\
  • References are released\
  • GC can reclaim memory\
  • Lifecycle boundaries are respected

This is modern Angular done correctly.


Signals: Why They Reduce Risk

readonly count = signal(0);

readonly double = computed(() => this.count() * 2);
Enter fullscreen mode Exit fullscreen mode

No manual unsubscribe.\
No subscription trees.\
No retained closures from manual streams.

Signals:

  • Track dependencies automatically\
  • Dispose reactive effects when destroyed\
  • Bind to component lifecycles

They reduce memory surface area dramatically.


Dangerous Patterns in Production

Global Event Listeners

window.addEventListener('resize', this.handleResize);
Enter fullscreen mode Exit fullscreen mode

Must be removed:

ngOnDestroy() {
  window.removeEventListener('resize', this.handleResize);
}
Enter fullscreen mode Exit fullscreen mode

Timers

private intervalId!: number;

ngOnInit() {
  this.intervalId = window.setInterval(() => this.refresh(), 1000);
}

ngOnDestroy() {
  clearInterval(this.intervalId);
}
Enter fullscreen mode Exit fullscreen mode

Intervals retain references indefinitely if not cleared.


Root Service Caches

@Injectable({ providedIn: 'root' })
export class DataCacheService {
  private cache: LargeDataset[] = [];
}
Enter fullscreen mode Exit fullscreen mode

Root services are never destroyed.\
Global memory must be intentional.


Production Rules I Follow

  • Always use takeUntilDestroyed()\
  • Prefer Signals for local state\
  • Avoid manual .subscribe() when async pipe works\
  • Clean up DOM listeners explicitly\
  • Audit singleton services\
  • Profile memory in Chrome regularly\
  • Stress-test long-lived dashboards

Memory leaks do not crash apps.

They erode trust.


Debugging Workflow (Senior-Level)

  1. Open Chrome DevTools\
  2. Memory tab → Heap Snapshot\
  3. Navigate repeatedly\
  4. Take second snapshot\
  5. Compare retained objects

If destroyed components still exist in memory, you have retained
references.

Inspect:

  • Closures\
  • Observables\
  • Event listeners\
  • Singleton services

Memory debugging is architectural thinking.


Interview Perspective

Interviewers expect:

  • Lifecycle awareness\
  • DestroyRef knowledge\
  • Understanding of retained references\
  • Real debugging experience\
  • Architectural ownership

Not just:

"We unsubscribe in ngOnDestroy."


Final Takeaway

Memory leaks don't fail fast.

They degrade slowly.

They damage trust quietly.

Angular 21 gives you tools:

  • DestroyRef\
  • takeUntilDestroyed\
  • Signals

Use them consistently.

Because in enterprise systems, performance is not optional.

It is a responsibility.

---\
Cristian Sifuentes\
Angular Performance Specialist

Top comments (0)