How to create, detect & fix a Memory Leak
in an Angular App
Garbage Collection
Mark
Sweep
marked=false
marked=false
marked=true
marked=true
marked=true
marked=true
Part I: Create The Memory Leak
Test Setup
- Sub Component shown / hidden periodically
@Component({
selector: 'app-root',
template: `<app-sub *ngIf="hide"></app-sub>`
})
export class AppComponent {
hide = false;
constructor() {
setInterval(() => this.hide = !this.hide, 50);
}
}
@Injectable()
export class DummyService {
behavior$ = new BehaviorSubject(2);
replay$ = new ReplaySubject(1);
private registeredComponents = [];
register(component) {
this.registeredComponents.push(component);
}
}
live.voxvote.com PIN: 65902
1.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
2.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
this.subject.subscribe();
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
3.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
this.subject.subscribe(() => {const bla = 55});
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
4.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
this.subject.subscribe(() => this.rand2 = 33);
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
5.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
this.dummyService.behavior$.subscribe();
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
6.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
this.dummyService.behavior$.subscribe(() => {const bla = 55});
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
7.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
this.dummyService.behavior$.subscribe(() => this.rand2 = 33);
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
8.) Is there a memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent {
rand = Math.random();
rand2 = 0;
subject = new BehaviorSubject(42);
arr = [];
constructor(private dummyService: DummyService) {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
this.dummyService.register(this);
}
}
@Injectable()
export class DummyService {
behavior$ = new BehaviorSubject(2);
replay$ = new ReplaySubject(1);
private registeredComponents = [];
register(component) {
this.registeredComponents.push(component);
}
}
Reminder: SubComponent is shown / hidden periodically with *ngIf
When is it a memory leak?
No Leak
ng
Component
window
Observable
ng
Component
window
Observable
Service
Subscribers
Leak
Subscribers
Answers
- no
- no
- no
- no
- yes, a small one
- yes, a small one
- yes, a big one
- yes a big one
When is it a memory leak?
- observable initialized in different class
- subscribe adds us to the list of subscribers
- subscription isn't unsubscribed
- although component is destroyed, the list of subscribers still exists (e.g. in service)
- great impact: subscription references component
.subscribe(() => this.doBla())
Conclusion
- complex and non-obvious situations lead to memory leaks
- tiny changes, possibly in non-obvious places like services, can have a great impact
How to avoid a subscription memory leak?
@Component({
selector:'app-sub',
template: 'mega {{rand}}'
})
export class SubComponent implements ngOnDestroy {
private destroy$ = new Subject<void>();
constructor(private dummyService: DummyService) {
this.dummyService.behavior$.pipe(
takeUntil(this.destroy$)
).subscribe((this => this.rand2 = 33));
this.dummyService.behavior$.pipe(
takeUntil(this.destroy$),
shareReplay(1),
takeUntil(this.destroy$)
).subscribe((this => this.rand2 = 33));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Recipe
- .subscribe()
- shareReplay()
- publishReplay()
- publish()
- publishBehavior()
must be preceded by
takeUntil(this.destroy$)
How to avoid a subscription memory leak?
npm install @mobi/rwc-lint-rules-jslib --save-dev
Use the "rxjs-prefer-angular-takeuntil-before-subscribe" rule
from @mobi/rwc-lint-rules-jslib
{
"extends": ["@mobi/rwc-lint-rules-jslib"],
"rules": {
"rxjs-prefer-angular-takeuntil-before-subscribe": { "severity": "error" },
...
}
}
ng lint
tslint.json
Danger Zone
export class BaseComponent implements OnDestroy {
private destroy$ = new Subject();
constructor(private dummyService: DummyService) {
this.dummyService.behavior$.pipe(takeUntil(this.destroy$))
.subscribe(() => this.doSomething());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
export class FancyComponent extends BaseComponent implements OnDestroy {
private _destroy$ = new Subject();
constructor(private dummyService: DummyService) {
this.dummyService.behavior$.pipe(takeUntil(this._destroy$))
.subscribe(() => this.doSomethingElse());
}
ngOnDestroy() {
this._destroy$.next();
this._destroy$.complete();
}
}
Danger Zone
export class BaseComponent implements OnDestroy {
private destroy$ = new Subject();
constructor(private dummyService: DummyService) {
this.dummyService.behavior$.pipe(takeUntil(this.destroy$))
.subscribe(() => this.doSomething());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
export class FancyComponent extends BaseComponent implements OnDestroy {
private _destroy$ = new Subject();
constructor(private dummyService: DummyService) {
this.dummyService.behavior$.pipe(takeUntil(this._destroy$))
.subscribe(() => this.doSomethingElse());
}
ngOnDestroy() {
super.ngOnDestroy();
this._destroy$.next();
this._destroy$.complete();
}
}
Further Reading
Part II: Detect The Memory Leak
Script
var count = 0;
var intervalTime = 10*1000; // 10s
var inter = setInterval(function(){
console.log("Leak Test: Click " + (count + 1));
var menuItems = $('.app-navigation-body .menu .menu-item');
menuItems[count % menuItems.length].click();
count++;
}, intervalTime);
// Stop it:
// clearInterval(inter)
Chrome Developer Tools
Memory Tab / Performance Monitor
Analyze the Heap
- Retained size: shallow size plus the shallow sizes of the objects that are accessible, directly or indirectly, only from this object.
- In other words, the retained size represents the amount of memory that will be freed by the garbage collector when this object is collected.
Analyze the Heap
- Retained size: shallow size plus the shallow sizes of the objects that are accessible, directly or indirectly, only from this object.
- In other words, the retained size represents the amount of memory that will be freed by the garbage collector when this object is collected.
Analyze the Heap
Analyze the Heap
With Memory Leak
Without Memory Leak
Analyze the Heap
With (subtle) Memory Leak
Analyze the Tab Memory
Important
- Close the Developer Tools
- Tab must be visible
Thanks for your attention!
Mobi: Create, Detect and Fix Memory Leak
By Esteban Gehring
Mobi: Create, Detect and Fix Memory Leak
- 897