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.



