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

  1. no
  2. no
  3. no
  4. no
  5. yes, a small one
  6. yes, a small one
  7. yes, a big one
  8. 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,009