How to create a Memory Leak
in an Angular App
Garbage Collection
Mark
Sweep
marked=false
marked=false
marked=true
marked=true
marked=true
marked=true
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);
}
}
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 blub = 34;});
}
}
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 blub = 34;});
}
}
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() and shareReplay() must be preceded by takeUntil(this.destroy$)
How to avoid a subscription memory leak?
npm install @angular-extensions/lint-rules --save-dev
Use the "angular-rxjs-takeuntil-before-subscribe" rule
from @angular-extensions/lint-rules
{
"extends": ["@angular-extensions/lint-rule"],
"rules": {
...
}
}
ng lint
tslint.sjon
Caveats
The rule is strict and in some situations takeUntil(this.destroy$) would not be nessary, e.g.
this.httpClient.get('/something')
.subscribe(result => this.something = result);
this.dummyService.willFireOnce$
.subscribe(result => this.something = result);
this.dummyService.replay$.pipe(
first()
).subscribe(result => this.something = result);
- observables that are guaranteed to complete
- observables created in the component
However, these situations are hard to analyze statically and without the rule, refactorings can introduce memory-leaks if not re-evaluated
Further Reading
Thanks for your attention!
Memory Leak
By Esteban Gehring
Memory Leak
- 2,183