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
Observables / Observer Pattern
new Observable(constructor_fn)
.subscribe(subscriber_fn)
const s = new Subject().subscribe(subscriber_fn)
s.next('some value')
register
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: 90566
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
- https://github.com/angular-extensions/lint-rules
- https://medium.com/angular-in-depth/how-to-create-a-memory-leak-in-angular-4c583ad78b8b
- https://github.com/macjohnny/angular-memory-leak-demo
- https://blog.angularindepth.com/the-best-way-to-unsubscribe-rxjs-observable-in-the-angular-applications-d8f9aa42f6a0
- https://github.com/cartant/rxjs-tslint-rules/pull/107
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!
SCS: Memory Leak
By Esteban Gehring
SCS: Memory Leak
- 733