Skip to content
Back to skills

Angular Memory Leaks

ASecurity

Detects and prevents Angular memory leaks. Covers subscription leaks, takeUntilDestroyed() (v16+), async pipe preference, DestroyRef, detached change-detector trees, DOM event listeners left attached, and NgZone.runOutsideAngular misuse. Includes a leak-hunt checklist.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 6, 2026
developmenttypescriptgoangular

Security analysis

A100/100

Scanned September 6, 2026

npx -y skills add AgenticPawan/FullStack-Pilot --skill angular-memory-leaks --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Angular Memory Leaks?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Angular Memory Leaks
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/agenticpawan-angular-memory-leaks/badge)](https://www.skillsdirectory.com/skills/agenticpawan-angular-memory-leaks)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: angular-memory-leaks
description: Detects and prevents Angular memory leaks. Covers subscription leaks, takeUntilDestroyed() (v16+), async pipe preference, DestroyRef, detached change-detector trees, DOM event listeners left attached, and NgZone.runOutsideAngular misuse. Includes a leak-hunt checklist.
when_to_use: memory leak, subscription cleanup, unsubscribe, takeUntil, destroyRef, component not destroyed, event listener cleanup, change detector detached, ngzone leak, subscription management, async pipe, leak hunt
applies_to: angular>=16
---

<!-- Version index:
  takeUntilDestroyed()   Angular 16+  (@angular/core/rxjs-interop)
  DestroyRef             Angular 16+
  inject(DestroyRef)     Angular 14+  (inject() in constructor context)
  afterRender/afterNextRender  Angular 17+
-->

## The four leak categories

| Category | Symptom | Fix |
|----------|---------|-----|
| Subscription not cleaned up | Observable keeps emitting after component destroyed | `takeUntilDestroyed()` or `async` pipe |
| DOM event listener not removed | Callback holds component reference alive | `DestroyRef.onDestroy()` cleanup |
| Detached ChangeDetectorRef | CD tree orphaned, still running | Detach in `ngOnDestroy`, mark `destroyed` |
| NgZone runOutsideAngular callback | Timer/WebSocket callback re-enters zone | Wrap zone entry in `ngZone.run()`, cancel in destroy |

---

## Subscription leaks

### BAD — subscribe() with no cleanup

```typescript
@Component({ ... })
export class DashboardComponent implements OnInit {
  data: Item[] = [];

  constructor(private svc: DataService) {}

  ngOnInit() {
    // Leak: subscription lives forever — component teardown never cancels it
    this.svc.updates$.subscribe(d => this.data = d);
  }
}
```

### GOOD — takeUntilDestroyed() (Angular 16+)

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

@Component({ ... })
export class DashboardComponent {
  data: Item[] = [];

  constructor(svc: DataService) {
    // takeUntilDestroyed() is called in injection context — no manual Subject needed
    svc.updates$.pipe(
      takeUntilDestroyed()
    ).subscribe(d => this.data = d);
  }
}
```

### ALSO GOOD — async pipe (zero-boilerplate, preferred for templates)

```typescript
@Component({
  template: `@for (item of data$ | async; track item.id) { ... }`
})
export class DashboardComponent {
  data$ = inject(DataService).updates$;
  // async pipe subscribes and unsubscribes automatically
}
```

### When to call takeUntilDestroyed() outside constructor

If the subscription must be created outside injection context (e.g., inside `ngOnInit`), inject `DestroyRef` and pass it explicitly:

```typescript
@Component({ ... })
export class ListComponent implements OnInit {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.svc.items$.pipe(
      takeUntilDestroyed(this.destroyRef)
    ).subscribe(items => this.items = items);
  }
}
```

---

## DOM event listener leaks

### BAD — addEventListener without removeEventListener

```typescript
@Component({ ... })
export class ResizeComponent implements OnInit {
  ngOnInit() {
    // Leak: handler holds `this` alive; never removed
    window.addEventListener('resize', () => this.onResize());
  }
}
```

### GOOD — register cleanup with DestroyRef

```typescript
@Component({ ... })
export class ResizeComponent {
  constructor() {
    const handler = () => this.onResize();
    window.addEventListener('resize', handler);
    inject(DestroyRef).onDestroy(() =>
      window.removeEventListener('resize', handler)
    );
  }
}
```

---

## NgZone leaks

### BAD — interval created outside zone, change detection spammed

```typescript
@Component({ ... })
export class PollComponent implements OnInit, OnDestroy {
  private intervalId: any;

  ngOnInit() {
    // Runs inside Angular zone — triggers CD on every tick
    this.intervalId = setInterval(() => this.poll(), 5000);
  }
  ngOnDestroy() { clearInterval(this.intervalId); }
}
```

### GOOD — run timer outside zone, re-enter only on data arrival

```typescript
@Component({ ... })
export class PollComponent {
  private ngZone  = inject(NgZone);
  private destroyRef = inject(DestroyRef);

  constructor(private svc: PollService) {
    let id: ReturnType<typeof setInterval>;

    this.ngZone.runOutsideAngular(() => {
      id = setInterval(() => {
        this.svc.fetch().then(data =>
          this.ngZone.run(() => this.data.set(data))  // re-enter zone for CD
        );
      }, 5000);
    });

    this.destroyRef.onDestroy(() => clearInterval(id));
  }
}
```

---

## Detached ChangeDetectorRef leak

```typescript
// BAD: detach without tracking or re-attaching
constructor(private cdr: ChangeDetectorRef) {
  this.cdr.detach();
  // Component never re-attaches or cleans up — CD tree orphaned
}

// GOOD: detach explicitly, mark for check on meaningful updates, clean up
constructor(private cdr: ChangeDetectorRef) {
  this.cdr.detach();
  inject(DestroyRef).onDestroy(() => this.cdr.detectChanges()); // flush final state
}
```

---

## Leak-hunt checklist

Run through this list when diagnosing a suspected leak in a component:

- [ ] **Subscriptions** — every `subscribe()` call is paired with `takeUntilDestroyed()` or `async` pipe
- [ ] **Subjects** — `Subject` and `BehaviorSubject` exposed as Observables are completed in `ngOnDestroy` or via `takeUntilDestroyed()`
- [ ] **DOM listeners** — every `addEventListener` has a matching `removeEventListener` registered via `DestroyRef.onDestroy()`
- [ ] **Timers** — `setInterval`/`setTimeout` IDs are cancelled in destroy; run outside Angular zone if they do not need CD
- [ ] **ViewRef** — `ApplicationRef.attachView()` calls are paired with `detachView()` on destroy
- [ ] **ChangeDetectorRef.detach()** — component re-attaches or calls `detectChanges()` before destroy
- [ ] **effect()** — returns a cleanup function if it attaches external listeners or timers
- [ ] **Router events** — subscriptions to `Router.events` use `takeUntilDestroyed()`
- [ ] **WebSocket** — `ws.close()` is called in `DestroyRef.onDestroy()`
- [ ] **afterRender/afterNextRender (v17+)** — cleanup returned if they register external effects

---

## Heap profiling quick-start (Chrome DevTools)

1. Open **Memory** tab → take baseline heap snapshot.
2. Navigate to the suspect component; interact; navigate away.
3. Take second snapshot; run GC.
4. Filter by `Component` class name — retained instances indicate a leak.
5. Expand the retaining tree to identify which reference keeps the component alive.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…