Volver

Dealing with memory leaks in ReactiveX

Diario del capitán, fecha estelar d658.y37/AB

ReactiveX Angular TypeScript Development Frontend
Frontend Developer
Dealing with memory leaks in ReactiveX

We build most of our web apps using Angular, to enhance the user experience and to treat data in a most effective way, and for this, we are using ReactiveX, an API for asynchronous programming with observable streams.

One annoying thing about ReactiveX (also called RxJS, in its JavaScript form) are its memory leaks because of Observables and Subscriptions. Here's what we do to deal with them.

The problem

Most of the Angular libraries are using Observables nowadays. There are multiple reasons to use them: they are flexible and powerful. Through Observables, you can manipulate data streams (map, scan, groupBy, filter, skip...), or combine different flows/Observables (join, zip, forkJoin...), although the most basic use cases are async requests to APIs.

For the sake of simplicity, let's say we need to retrieve a list of books. We will use this as an example.

We would write it this way, if we were using Promises:

http.get('http://sample.com/books').then((books) =>
  processBooks(books)
).catch((error) => handleError(error));

Or else, if we preferred to go for async/await:

try {
  const books = await http.get('http://sample.com/books');
  processBooks(books);
} catch (err) {
  handleError(err);
}

If we'd rather go for the Observables way, we'd do it as follows:

http.get('http://sample.com/books').subscribe((books) =>
  processBooks(books)
).catch((error) => handleError(error));

As you can see, the Observables and Promises codes are pretty similar.

We can use Observables to achieve different things, let's say we have a service that provides an observable that notifies us when the data's current user changes. We can subscribe to it in the initialization hook of the component (Angular lifecycle hooks):

@Component({
  selector: 'info-container'
  templateUrl: './info-container.html'
})
export class InfoContainerComponent implements OnInit {
  user: User;

  constructor(private userService: UserService){ }

  ngOnInit(): void {
    this.subscribeToUserChanges();
  }

  private subscribeToUserChanges(): void {
    userService.user$.subscribe((user) =>
      this.user = user;
    )
  }
}

The code above is similar to Promises, but it's not a Promise. In fact, it's really different: once you have created a subscription, it will watch for changes until you perform an unsubscribe() operation. This consumes memory and CPU time.

In the previous example, if the InfoContainerComponent is destroyed, the user subscription will remain active forever, executing the code when the event subscription triggers. Not only this will cause a higher memory consumption but also it could cause errors or unexpected behavior.

You have to manage the subscription. This reminds us of the C and C++ memory allocation, when you need to explicitly free the memory you don't need anymore.

int *p = malloc(sizeof(int));
...
free(p)

How to solve it

The first solution in Angular is to save the subscription reference and destroy it with the component.

@Component({
  selector: 'info-container'
  templateUrl: './info-container.html'
})
export class InfoContainerComponent implements OnInit, OnDestroy {
  user: User;

  private userSubscription: Subscription;

  constructor(private userService: UserService){ }

  ngOnInit(): void {
    this.subscribeToUserChanges();
  }

  ngOnDestroy(): void {
    this.userSubscription() && this.userSubscription.unsubscribe();
  }

  private subscribeToUserChanges(): void {
    this.userSubscription = userService.user$.subscribe((user) =>
      this.user = user;
    )
  }
}

If you are only managing one Subscription, you don't need to do more. This is enough.

But sometimes, things are not so simple... and we have multiple Observables and Subscriptions inside the same Angular component:

@Component({
  selector: 'info-container'
  templateUrl: './info-container.html'
})
export class InfoContainerComponent implements OnInit, OnDestroy {
  user: User;
  position: Position;
  lastNotification: string;

  private userSubscription: Subscription;
  private positionSubscription: Subscription;
  private notificationSubscription: Subscription;

  constructor(
    private userService: UserService,
    private positionService: PositionService,
    private notificationService: NotificationService
  ){ }

  ngOnInit(): void {
    this.subscribeToUserChanges();
    this.subscribeToCurrentPosition();
    this.subscribeToNotifications();
  }

  ngOnDestroy(): void {
    this.userSubscription() && this.userSubscription.unsubscribe();
    this.positionSubscription() && this.positionSubscription.unsubscribe();
    this.notificationSubscription() && this.notificationSubscription.unsubscribe();
  }

  // Subscribe to User, Position, and Notification
  ...
}

This code is not scalable at all. Thus, we need to find an alternative to deal with Subscriptions.

We could use an array to save all the subscriptions we are creating:

@Component({
  selector: 'info-container'
  templateUrl: './info-container.html'
})
export class InfoContainerComponent implements OnInit, OnDestroy {
  user: User;
  position: Position;
  lastNotification: string;

  private subscriptions: Subscription[] = [];

  constructor(
    private userService: UserService,
    private positionService: PositionService,
    private notificationService: NotificationService
  ){ }

  ngOnInit(): void {
    this.subscribeToUserChanges();
    this.subscribeToCurrentPosition();
    this.subscribeToNotifications();
  }

  ngOnDestroy(): void {
    this.subscriptions.forEach((subscription: Subscription) =>
      subscription.unsubscribe();
    );
  }

  private subscribeToUserChanges(): void {
    this.subscriptions.push(userService.user$.subscribe((user) =>
      this.user = user;
    ));
  }

  // Susbcribe to Position, and Notification
  ...
}

This solution is good because we can keep references of each and every subscription. We can even use a hash for a better access.

Most of the time, we don't need to keep these references in memory, so we can use the RxJS takeUntil() to stop subscription execution when the component is destroyed.

takeUntil is an operator to stop and terminate an Observable when a second Observable is marked as complete or it has emitted a value. We can use it to compose our subscribers with another Subject. This new Subject will act as a kind of semaphore to allow or not the original Observable to emit a value.

The key here, is to mark as complete the Subject when our component is destroyed, this will cause to mark as complete every subscriber that is using takeUntil(). From then on, subscribers will not be executed.

@Component({
  selector: 'info-container'
  templateUrl: './info-container.html'
})
export class InfoContainerComponent implements OnInit, OnDestroy {
  user: User;
  position: Position;
  lastNotification: string;

  private ngUnsubscribe: Subject<any> = new Subject();

  constructor(
    private userService: UserService,
    private positionService: PositionService,
    private notificationService: NotificationService
  ){ }

  ngOnInit(): void {
    this.subscribeToUserChanges();
    this.subscribeToCurrentPosition();
    this.subscribeToNotifications();
  }

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

  private subscribeToUserChanges(): void {
    userService.user$
      .takeUntil(this.ngUnsubscribe)
      .subscribe((user) =>
        this.user = user;
      )
  }

  // Subscribe to Position, and Notification
  ...
}

The above code is using a new object ngUnsubscribe, so takeUntil will monitor it and stop sending values to subscribe when ngUnsubscrible is completed (ngOnDestroy).

This method is optimal:

This is the official recommendation of the Angular team.

Angular HTTP

The most common use for Observables is Angular HttpClient, and some libraries, like this, return finite Observables. These observables are already marked as complete, so you don't need to worry about unsubscriptions.

Read more

If you're interested in learning more about this, you should read these threads from Stackoverflow:

Compartir este post

Artículos relacionados

Change detection strategy in Angular

Change detection strategy in Angular

How to change the detection strategy in Angular to improve performance and to adapt it to your project requirements.

Leer el artículo
Comparing the three Angular view encapsulation methods

Comparing the three Angular view encapsulation methods

Since we started the company, we have been using Angular for most of our frontend development projects. In this blog post, we share how we encapsulate the views in Angular projects.

Leer el artículo
Our curated Angular reference guide

Our curated Angular reference guide

We share useful Angular resources, questions, experiences, tools, guides and tutorials to keep this curated Angular reference post up-to-date. Enjoy it!

Leer el artículo