Angular comes with a powerful concept known as Observables. This article will help you avoid one of the most common usage mistakes in your code: memory leaks.
At Sipios, we want to create websites that are appreciated by users, with the aim that they will recommend them to their friends and family. This is achieved through the responsiveness of the pages, their dynamism, and their response time. To do this, we use asynchronism. In JS, Promises manage asynchronous functions. Let's start by seeing the difference with observables!
Observables vs. Promises
- Promises: can only emit one value. It can have three states:
- Pending: initial state, neither fulfilled nor rejected;
- Fulfilled: meaning that the operation was completed successfully;
- Rejected: meaning that the operation failed;
Once the promise is fulfilled or rejected, it ends. It thus allows us to obtain a unique asynchronous response.
- Observable: will listen for changes on a stream and emit them. To start listening to this data stream, it will be necessary to subscribe to it. To stop listening, we unsubscribe the observable. It is very useful when handling variables that are updated asynchronously on the same page, making calls to the back end regularly, etc.
In the example below, the interval function is an observable that will transmit every 200 milliseconds to simulate a change in the flow:
- Subject: it’s a subtype of the Observable type, bidirectional. This means that it allows us to update the value of the observable with which it is associated, ourselves, thanks to the next method.
What is a memory leak?
The main advantage of observables is that they don’t destroy themselves after the first update of the variable. This characteristic can lead to a huge risk if we forget to stop listening: it will create a memory leak.
To illustrate this, we will create a simple website with one button “Trigger”. When we click on it, a list of 10 000 random numbers will be created every second. On our page, we will see the sum of each list. After a second click on our button, the component will be destroyed and lists too.
Here is the memory leak: when we click on the button for the first time, we create a child component that manages the lists. To do that, an observable will be triggered every second, and with that, the creation of a list. After the second click on the button, the child component will be destroyed, but not the observable because we didn’t clearly say that we would like to unsubscribe from it. That’s why, when we make the child component appear again, a new observable is created in addition to the old one. That’s why, the more we trigger the button, the more we see multiple values appear concurrently on our screen.
How to detect them?
The first clue for a memory leak is…. the response time. The more actions we make on our website, the more time it takes to navigate between pages. In that case, you may just have found a leak. If the memory leak is caused by callbacks, you can verify this by seeing if you have duplicated calls in the Network view of your browser. Two other views that can help you detect leaks in other cases are:
- Performance view: allows you to see if the number of used listeners is increasing. Using the example above, we obtain: https://www.loom.com/share/ab1db60767b04088822924dc067fd6d0
- Memory view: gives you an idea of what is causing the memory leak. It will give you access to snapshot comparisons of the browser to know the deltas on the offending objects: https://www.loom.com/share/881bac1422644840afc75e95ee20af3c
How to avoid them?
Using unsubscribe is the basic method, the most known. The principle is quite simple: we store our subscribes in objects of type Subscription. And we destroy them when we want to stop listening to them. Most commonly, the destruction is done in the ngOnDestroy method (which is the last step of an Angular component lifecycle).
It is possible to use the add method of the Subscription class to chain several subscriptions. Doing so, if we destroy the first observable of the chain, all those that follow will be destroyed. This avoids writing the unsubscribe line several times. Of course, this method destroys all observables at once, so it is not to be used in all situations.
Take, first, etc
Another method to manage subscriptions in a controlled way is the use of RxJs functions in the pipe() of the observable. There are several of them that allow managing each observable in a unitary way: first(), take(), takeUntil() and takeWhile(). Let's try to understand their respective behaviors:
- First: it allows retrieving only the first value that comes from an observable that respects the indicated condition (otherwise, it only takes the first one). This is very useful when you want to retrieve, for example, data about an authentication. We will create an observable to manage the asynchronism for the time it takes to check a role and as soon as the information is returned, we will destroy the observable.
WARNING: if you put a predicate and there is no match, an exception is thrown!
- Take: it allows retrieving the first x values of an observable before destroying it. It is often recommended to use a take(1) rather than a first() if you want the first value emitted by the observable. Indeed, first() is preferred when you have some logic (other than the order of emission) in the selection, and it is more likely to cause mistakes.
- TakeUntil: it is certainly one of the most useful tools to manage observables efficiently in RxJs operators. It allows putting a destruction condition in the form of a Subject. In complex applications where the observables have the same life cycle, this solution is really to be recommended compared to unsubscribe, by setting the Subject to true when the component is destroyed.
- TakeWhile: it works as a filter on the values of the observable. It is rarely used in practice, except if you want to filter the returns of an observable. For example, we launch a call on a Meteo France API to retrieve the temperatures of August, but we want to retrieve the temperatures until the first one below 15 °C.
Without a doubt, it is the cleanest and most efficient (ahead of takeUntil) because it limits the amount of code written. It limits the forgetting because it associates the life cycle of the observable to the one of the HTML. You don't need to subscribe in the ".ts" and you call the observable at the place where you want to use it by adding "| async".
Even if this method is the cleanest, it has its limits! As explained, the observable is solved locally. So, if you need to use the value in widely spaced places in the HTML, to avoid making the HTML more cumbersome, you should use one of the options above. This will avoid duplicating the code.
Observables are powerful tools for a dynamic website. But using them in an uncontrolled way could greatly decrease the performance of your website. You can anticipate memory leak problems by using different functions:
- if you use it locally in the HTML: | async
- if you have more than one observable with the same life cycle: takeUntil
- if you only want the first value emitted or the nth ones: take
- if you have a specific life cycle logic: first, takeWhile, takeUntil
On the Bpifrance website, learning about memory leaks has allowed us to solve a lot of bugs and save up to 40% of loading time on certain pages.