How to cause a memory leak in an Angular application?

Performance is the key to the success of a web application. Therefore, developers need to know how memory leaks occur and how to deal with them.

This knowledge is especially important when the application the developer is dealing with reaches a certain size. If you do not pay enough attention to memory leaks, then everything may end up with the developer getting into the “team for eliminating memory leaks” (I had to be part of such a team). Memory leaks can occur for various reasons. However, I believe that when using Angular, you may encounter a pattern that matches the most common cause of memory leaks. There is a way to deal with such memory leaks. And the best thing, of course, is not to fight problems, but to avoid them.





What is memory management?


JavaScript uses an automatic memory management system. The memory life cycle usually consists of three steps:

  1. Allocation of necessary memory.
  2. Work with allocated memory, performing read and write operations.
  3. Releasing memory after it is no longer needed.

On MDN says that automatic memory management - it is a potential source of confusion. This can give developers a false sense that they do not need to worry about memory management.

If you do not care about memory management at all, this means that after your application grows to a certain size, you may well encounter a memory leak.

In general, memory leaks can be thought of as the memory allocated to the application, which it no longer needs, but is not released. In other words, these are objects that failed to undergo garbage collection operations.

How does garbage collection work?


During the garbage collection procedure, which is quite logical, all that can be considered “garbage” is cleaned. The garbage collector cleans up memory that the application no longer needs. In order to find out what areas of memory the application still needs, the garbage collector uses the “mark and sweep” algorithm (tagging algorithm). As the name implies, this algorithm consists of two phases - the marking phase and the sweep phase.

▍ Flag phase


Objects and links to them are presented in the form of a tree. The root of the tree is, in the following figure, a node root. In JavaScript, this is an object window. Each object has a special flag. Let's name this flag marked. In the flagging phase, first of all, all flags markedare set to a value false.


At the beginning, the flags of marked objects are set to false.

Then the object tree is traversed. All flags ofmarkedobjects reachable from the noderootare set totrue. And the flags of those objects that cannot be reached, remain in the valuefalse.

An object is considered unreachable if it cannot be reached from the root object.


Reachable objects are marked as marked = true, unreachable objects as marked = false

As a result, all flags ofmarkedunreachable objects remain in the valuefalse. The memory has not yet been freed, but, after the completion of the tagging phase, everything is ready for the cleaning phase.

▍ Cleaning phase


The memory is cleared precisely at this phase of the algorithm. Here, all unreachable objects (those whose flag markedremains in the value false) are destroyed by the garbage collector.


Object tree after garbage collection. All objects whose marked flag is set to false are destroyed by the garbage collector.

Garbage collection is periodically performed while the JavaScript program is running. During this procedure, memory is released that can be freed.

Perhaps the following question arises here: “If the garbage collector removes all objects marked as unreachable - how to create a memory leak?”.

The point here is that the object will not be processed by the garbage collector if the application does not need it, but you can still reach it from the root node of the object tree.

The algorithm cannot know whether the application will use some piece of memory that it can access or not. Only a programmer has such knowledge.

Angular memory leaks


Most often, memory leaks occur over time when a component is repeatedly re-rendered. For example - through routing, or as a result of using the directive *ngIf. Say, in a situation where some advanced user works with the application all day without updating the application page in the browser.

In order to reproduce this scenario, we will create a construction of two components. These will be the components AppComponentand SubComponent.

@Component({
  selector: 'app-root',
  template: `<app-sub *ngIf="hide"></app-sub>`
})
export class AppComponent {
  hide = false;

  constructor() {
    setInterval(() => this.hide = !this.hide, 50);
  }
}

The component template AppComponentuses the component app-sub. The most interesting thing here is that our component uses a function setIntervalthat switches the flag hideevery 50 ms. This results in a component being re-rendered every 50 ms app-sub. That is, the creation of new instances of the class is performed SubComponent. This code mimics the behavior of a user who works all day with a web application without refreshing a page in a browser.

We, in SubComponent, have implemented different scenarios, in the use of which, over time, changes in the amount of memory used by the application begin to appear. Note that the componentAppComponentalways remains the same. In each scenario, we will find out if what we are dealing with is a memory leak by analyzing the memory consumption of the browser process.

If the memory consumption of the process increases over time, this means that we are faced with a memory leak. If a process uses a more or less constant amount of memory, it means either that there is no memory leak, or that the leak, although present, does not manifest itself in a fairly obvious way.

▍ Scenario # 1: huge for loop


Our first scenario is represented by a loop that runs 100,000 times. In the loop, random values ​​are added to the array. Let's not forget that the component is re-rendered every 50 ms. Take a look at the code and think about whether we created a memory leak or not.

@Component({
  selector:'app-sub',
  // ...
})
export class SubComponent {
  arr = [];

  constructor() {
    for (let i = 0; i < 100000; ++i) {
      this.arr.push(Math.random());
    }
  }
}

Although such code should not be sent to production, it does not create a memory leak. Namely, the memory consumption does not go beyond the range limited to a value of 15 MB. As a result, there is no memory leak. Below we will talk about why this is so.

▍ Scenario 2: BehaviorSubject Subscription


In this scenario, we subscribe to BehaviorSubjectand assign a value to a constant. Is there a memory leak in this code? As before, do not forget that the component is rendered every 50 ms.

@Component({
    selector:'app-sub',
    // ...
})
export class SubComponent {
  subject = new BehaviorSubject(42);
  
  constructor() {
    this.subject.subscribe(value => {
        const foo = value;
    });
  }
}

Here, as in the previous example, there is no memory leak.

▍ Scenario 3: assigning a value to a class field inside a subscription


Here, almost the same code is presented as in the previous example. The main difference is that the value is assigned not to a constant, but to a class field. And now, do you think there is a leak in the code?

@Component({
    selector:'app-sub',
    // ...
})
export class SubComponent {
  subject = new BehaviorSubject(42);
  randomValue = 0;
  
  constructor() {
    this.subject.subscribe(value => {
        this.randomValue = value;
    });
  }
}

If you believe that there is no leak here - you are absolutely right.

In scenario # 1 there is no subscription. In scenarios No. 2 and 3, we subscribed to the stream of the observed object initialized in our component. It feels like we're safe by subscribing to component flows.

But what if we add service to our scheme?

Scenarios that use the service


In the following scenarios, we are going to revise the above examples, but this time we will subscribe to the stream provided by the service DummyService. Here is the service code.

@Injectable({
  providedIn: 'root'
})
export class DummyService {

   some$ = new BehaviorSubject<number>(42);
}

Before us is a simple service. This is just a service that provides stream ( some$) in the form of a public class field.

▍ Scenario 4: Subscribing to a stream and assigning a value to a local constant


We will recreate here the same scheme that was already described earlier. But this time, we subscribe to the stream some$from DummyService, and not to the component’s field.

Is there a memory leak? Again, when answering this question, remember that the component is used in AppComponentand rendered many times.

@Component({
    selector:'app-sub',
    // ...
})
export class SubComponent {
  
  constructor(private dummyService: DummyService) {
    this.dummyService.some$.subscribe(value => {
        const foo = value;
    });
  }
}

And now we finally created a memory leak. But this is a small leak. By "small leak" I mean one that, over time, leads to a slow increase in the amount of memory consumed. This increase is barely noticeable, but a cursory inspection of the heap snapshot showed the presence of many undeleted instances Subscriber.

▍ Scenario 5: subscribing to a service and assigning a value to a class field


Here we subscribe again to dummyService. But this time we assign the resulting value to the class field, and not a local constant.

@Component({
    selector:'app-sub',
    // ...
})
export class SubComponent {
  randomValue = 0;
  
  constructor(private dummyService: DummyService) {
    this.dummyService.some$.subscribe(value => {
        this.randomValue = value;
    });
  }
}

And here we finally created a significant memory leak. Memory consumption quickly, within a minute, exceeded 1 GB. Let's talk about why this is so.

▍When did a memory leak occur?


You may have noticed that in the first three scenarios we were not able to create a memory leak. These three scenarios have something in common: all links are local to the component.

When we subscribe to an observable object, it stores a list of subscribers. Our callback is also on this list, and the callback can refer to our component.


No memory leak

When a component is destroyed, that is, when Angular no longer has a link to it, which means that the component cannot be reached from the root node, the observed object and its list of subscribers cannot be reached from the root node either. As a result, the entire component object is garbage collected.

As long as we are subscribed to an observable object, links to which are only within the component, no problems arise. But when the service comes into play, the situation changes.


Memory Leak

As soon as we subscribed to an observable object provided by a service or another class, we created a memory leak. This is due to the observed object, because of its list of subscribers. Because of this, the callback, and therefore the component, are accessible from the root node, although Angular does not have a direct reference to the component. As a result, the garbage collector does not touch the corresponding object.

I’ll clarify: you can use such constructions, but you need to work with them correctly, and not like we do.

Proper Subscription Work


In order to avoid memory leak, it is important to unsubscribe correctly from the observed object, by doing this when the subscription is no longer needed. For example, when a component is destroyed. There are many ways to unsubscribe from an observed object.

The experience of advising owners of large corporate projects indicates that in this situation it is best to use the entity destroy$created by the team new Subject<void>()in combination with the operator takeUntil.

@Component({
  selector:'app-sub',
  // ...
})
export class SubComponent implements OnDestroy {

  private destroy$: Subject<void> = new Subject<void>();
  randomNumber = 0;

  constructor(private dummyService: DummyService) {
      dummyService.some$.pipe(
          takeUntil(this.destroy$)
      ).subscribe(value => this.randomNumber = value);
  }

  ngOnDestroy(): void {
      this.destroy$.next();
      this.destroy$.complete();
  }
}

Here we unsubscribe from the subscription using the destroy$and operator takeUntilafter the destruction of the component.

We implemented a lifecycle hook in the component ngOnDestroy. Every time a component is destroyed, we call destroy$methods nextand complete.

The call is completevery important because this call clears the subscription from destroy$.

Then we use the operator takeUntiland pass it our stream destroy$. This ensures that the subscription is cleared (that is, that we have unsubscribed from the subscription) after the component is destroyed.

How to remember to clear subscriptions?


It's easy to forget to add in the component destroy$and forget to call next, and completein Hook lifecycle ngOnDestroy. Even despite the fact that I taught this to teams working on projects, I often forgot about it myself.

Fortunately, there is a wonderful rule of linter, which is part of a set of rules that allows you to ensure proper unsubscription from subscriptions. You can set a rule set like this:

npm install @angular-extensions/lint-rules --save-dev

Then it must be connected to tslint.json:

{
  "extends": [
    "tslint:recommended",
    "@angular-extensions/lint-rules"
  ]
}

I highly recommend that you use this set of rules in your projects. This will save you many hours of debugging in finding sources of memory leaks.

Summary


In Angular, it is very easy to create a situation leading to memory leaks. Even small code changes in places that, apparently, should not be related to memory leaks, can lead to serious adverse consequences.

The best way to avoid memory leaks is to manage your subscriptions correctly. Unfortunately, the operation of cleaning subscriptions requires great accuracy from the developer. This is easy to forget. Therefore, it is recommended that you apply rules @angular-extensions/lint-rulesthat help you organize the right work with your subscriptions.

Here is the repository with the code underlying this material.

Have you encountered memory leaks in Angular applications?


All Articles