How to handle memory leaks in Angular

Handling memory leaks in Angular requires proper subscription management, event listener cleanup, and component lifecycle awareness. As the creator of CoreUI with over 12 years of Angular experience since 2014, I’ve identified and fixed memory leaks in numerous enterprise applications. Angular memory leaks commonly occur from unsubscribed observables, unremoved event listeners, and retained component references. This approach ensures applications remain performant with stable memory usage even during extended sessions.

Unsubscribe from observables in ngOnDestroy using takeUntil, async pipe, or manual subscription management to prevent memory leaks.

Using takeUntil pattern:

// user.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core'
import { Subject, takeUntil } from 'rxjs'
import { UserService } from './user.service'

@Component({
  selector: 'app-user',
  template: `
    <div *ngIf="user">{{ user.name }}</div>
  `
})
export class UserComponent implements OnInit, OnDestroy {
  user: any
  private destroy$ = new Subject<void>()

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    // Automatically unsubscribe on destroy
    this.userService.getUser()
      .pipe(takeUntil(this.destroy$))
      .subscribe(user => this.user = user)

    this.userService.getNotifications()
      .pipe(takeUntil(this.destroy$))
      .subscribe(notifications => console.log(notifications))
  }

  ngOnDestroy(): void {
    this.destroy$.next()
    this.destroy$.complete()
  }
}

Using async pipe (recommended):

// user-list.component.ts
import { Component } from '@angular/core'
import { Observable } from 'rxjs'
import { UserService } from './user.service'

interface User {
  id: number
  name: string
  email: string
}

@Component({
  selector: 'app-user-list',
  template: `
    <div *ngFor="let user of users$ | async">
      {{ user.name }} - {{ user.email }}
    </div>
  `
})
export class UserListComponent {
  users$: Observable<User[]>

  constructor(private userService: UserService) {
    // No manual subscription needed
    // async pipe handles subscribe/unsubscribe automatically
    this.users$ = this.userService.getUsers()
  }
}

Manual subscription management:

// dashboard.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core'
import { Subscription } from 'rxjs'
import { DataService } from './data.service'

@Component({
  selector: 'app-dashboard',
  template: `<div>{{ data }}</div>`
})
export class DashboardComponent implements OnInit, OnDestroy {
  data: string = ''
  private subscriptions = new Subscription()

  constructor(private dataService: DataService) {}

  ngOnInit(): void {
    // Add subscriptions to container
    this.subscriptions.add(
      this.dataService.getData().subscribe(data => {
        this.data = data
      })
    )

    this.subscriptions.add(
      this.dataService.getUpdates().subscribe(update => {
        console.log(update)
      })
    )
  }

  ngOnDestroy(): void {
    // Unsubscribe all at once
    this.subscriptions.unsubscribe()
  }
}

Event listener cleanup:

// scroll-tracker.component.ts
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'

@Component({
  selector: 'app-scroll-tracker',
  template: `<div>Scroll position: {{ scrollPosition }}</div>`
})
export class ScrollTrackerComponent implements OnInit, OnDestroy {
  scrollPosition = 0
  private scrollHandler: () => void

  ngOnInit(): void {
    // Add event listener
    this.scrollHandler = () => {
      this.scrollPosition = window.scrollY
    }
    window.addEventListener('scroll', this.scrollHandler)
  }

  ngOnDestroy(): void {
    // Remove event listener to prevent leak
    window.removeEventListener('scroll', this.scrollHandler)
  }
}

// Alternative: Using @HostListener (auto-cleanup)
@Component({
  selector: 'app-scroll-tracker-auto',
  template: `<div>Scroll: {{ scrollPosition }}</div>`
})
export class ScrollTrackerAutoComponent {
  scrollPosition = 0

  @HostListener('window:scroll')
  onScroll(): void {
    this.scrollPosition = window.scrollY
    // Automatically cleaned up when component destroyed
  }
}

Timer cleanup:

// timer.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core'

@Component({
  selector: 'app-timer',
  template: `<div>Time: {{ time }}</div>`
})
export class TimerComponent implements OnInit, OnDestroy {
  time = 0
  private intervalId: any

  ngOnInit(): void {
    this.intervalId = setInterval(() => {
      this.time++
    }, 1000)
  }

  ngOnDestroy(): void {
    // Clear interval to prevent leak
    if (this.intervalId) {
      clearInterval(this.intervalId)
    }
  }
}

Detaching change detection:

// heavy-computation.component.ts
import { Component, ChangeDetectorRef, OnDestroy } from '@angular/core'

@Component({
  selector: 'app-heavy-computation',
  template: `<div>Result: {{ result }}</div>`
})
export class HeavyComputationComponent implements OnDestroy {
  result = 0

  constructor(private cdr: ChangeDetectorRef) {}

  startComputation(): void {
    this.cdr.detach()

    // Perform heavy computation
    setTimeout(() => {
      this.result = Math.random()
      this.cdr.reattach()
      this.cdr.detectChanges()
    }, 2000)
  }

  ngOnDestroy(): void {
    // Ensure detached change detector is cleaned up
    this.cdr.detach()
  }
}

Common memory leak patterns:

// ❌ BAD - Memory leak
@Component({
  selector: 'app-bad-component'
})
export class BadComponent implements OnInit {
  constructor(private userService: UserService) {}

  ngOnInit(): void {
    // Never unsubscribed
    this.userService.getUsers().subscribe(users => {
      console.log(users)
    })

    // Event listener never removed
    window.addEventListener('resize', () => {
      console.log('Resized')
    })

    // Interval never cleared
    setInterval(() => {
      console.log('Tick')
    }, 1000)
  }
}

// ✅ GOOD - No memory leaks
@Component({
  selector: 'app-good-component'
})
export class GoodComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>()
  private intervalId: any

  constructor(private userService: UserService) {}

  ngOnInit(): void {
    // Properly unsubscribed
    this.userService.getUsers()
      .pipe(takeUntil(this.destroy$))
      .subscribe(users => console.log(users))

    // Using @HostListener instead of manual addEventListener
    this.intervalId = setInterval(() => {
      console.log('Tick')
    }, 1000)
  }

  @HostListener('window:resize')
  onResize(): void {
    console.log('Resized')
  }

  ngOnDestroy(): void {
    this.destroy$.next()
    this.destroy$.complete()
    clearInterval(this.intervalId)
  }
}

Debugging memory leaks:

// memory-leak-detector.service.ts
import { Injectable, OnDestroy } from '@angular/core'

@Injectable()
export class MemoryLeakDetectorService implements OnDestroy {
  private componentName: string
  private createdAt: number

  constructor(componentName: string) {
    this.componentName = componentName
    this.createdAt = Date.now()
    console.log(`${componentName} created at ${this.createdAt}`)
  }

  ngOnDestroy(): void {
    const lifespan = Date.now() - this.createdAt
    console.log(`${this.componentName} destroyed after ${lifespan}ms`)
  }
}

// Usage
@Component({
  selector: 'app-test',
  providers: [
    { provide: MemoryLeakDetectorService, useValue: new MemoryLeakDetectorService('TestComponent') }
  ]
})
export class TestComponent {}

Using DestroyRef (Angular 16+):

// modern-component.component.ts
import { Component, OnInit, inject, DestroyRef } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { UserService } from './user.service'

@Component({
  selector: 'app-modern',
  template: `<div>{{ user?.name }}</div>`
})
export class ModernComponent implements OnInit {
  user: any
  private destroyRef = inject(DestroyRef)
  private userService = inject(UserService)

  ngOnInit(): void {
    // Automatically unsubscribes when component destroyed
    this.userService.getUser()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(user => this.user = user)
  }
}

Memory leak testing:

// component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { UserComponent } from './user.component'

describe('UserComponent Memory Leaks', () => {
  let component: UserComponent
  let fixture: ComponentFixture<UserComponent>

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [UserComponent]
    })
    fixture = TestBed.createComponent(UserComponent)
    component = fixture.componentInstance
  })

  it('should clean up subscriptions on destroy', () => {
    const destroySpy = jest.spyOn(component['destroy$'], 'next')
    const completeSpy = jest.spyOn(component['destroy$'], 'complete')

    fixture.destroy()

    expect(destroySpy).toHaveBeenCalled()
    expect(completeSpy).toHaveBeenCalled()
  })
})

Best Practice Note

Use async pipe for automatic subscription management—it handles unsubscribe automatically. Use takeUntil pattern with Subject for manual subscriptions. Clean up event listeners in ngOnDestroy. Clear intervals and timeouts when component destroys. Use @HostListener for automatic event cleanup. Detach and reattach change detection properly. Use DestroyRef and takeUntilDestroyed in Angular 16+. Test component destruction to verify cleanup. This is how we prevent memory leaks in CoreUI Angular components—async pipe everywhere possible, takeUntil for manual subscriptions, and thorough cleanup in ngOnDestroy ensuring stable memory usage even in long-running applications.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.

Answers by CoreUI Core Team