Angular tips: async pipe


Today I want to share one important thing about usage of RxJS streams and async pipe in Angular applications. I still face some misunderstanding here or even the facts that people don’t suspect any potential issues with the code.

It’ll be very boring and probably not very useful if I just show code sample with unexpected behavior and code sample with expected behavior, because it might look like “Why are you doing it at all? Isn’t the code looks weird?”. So yeah, I’ll start from the preconditions and use case to explain what I wanted to achieve and how I wanted to achieve it.

Use Case

  • On the project I’m using Angular as a main framework (as you can guess from the title) and ngrx as a primary data management tool;
  • 95% of the components I have use ChangeDetectionStrategy.OnPush to avoid any redundant change detection cycles. As a result I use a lot of async pipes and RxJS streams directly from ngrx Store;
  • In the project I have list of filters which user can apply to narrow the search scope. These filters are just a plain list however we want to group them by some criteria and show as a expand/collapse tree to improve navigation and UX;

solution

As a result I wanted to implement a solution, where filters are stored in the ngrx Store, because in all other parts of application it’s much more convenient to use these filters as a plain list to be able to quickly filter them out.

On the other hand component which is responsible for rendering filters as an expand/collapse tree will receive the stream from the ngrx Store, apply the transformation to the stream’s values and recursively render all the branches of the resulting tree.

Now we are finally ready to see some code. The code snippet looks quite big, but I’ve tried to make it as small as possible to remove all noise and leave only the problem itself.

import { Component } from '@angular/core';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-root',
template: '<app-tree [data]="data"></app-tree>'
})
export class AppComponent {
data = of({
title: 'aaa',
value: 2,
items: [
{ title: 'bbb', value: 4 },
{ title: 'ccc', value: 6 },
{ title: 'ddd', value: 8 }
]
}).pipe(
map((data) => {
console.warn('Do some lightweight operation');
return data;
})
);
}
import { Component, OnInit, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-tree',
template: `
<div *ngIf="data | async as data">{{data.title}}: {{data.value}}</div>
<ng-container *ngIf="data | async as data">
<ng-container *ngIf="data.items">
<app-tree *ngFor="let item of data.items; index as i" [data]="getSubtree(i)">
</app-tree>
</ng-container>
</ng-container>
`
})
export class TreeComponent {
@Input() data: Observable<any>;
getSubtree(i: number) {
return this.data.pipe(
map((data) => data.items[i])
);
}
}
view raw angular-tree.ts hosted with ❤ by GitHub

problem

When I’ve finished up with the code I noticed that the performance of this component is quite low (number of items in the production’s filters list was around 10 thousands). I was surprised, because rendering even 10 thousands of items should not be a problem (in this article I will not discuss the fact, that there are ways to avoid rendering huge DOM trees, for example, to render only visible parts of huge lists).

I’ve started to investigate the problem and found that actually the most of CPU time was spend inside map function, the function which is responsible for the initial list-to-tree transformation. This function was called around 100.000 times. In fact my expectation was that it will be called only once. What a surprise! If you run the sample application you’ll notice the same problem

map function was called 13 time

fix

To fix the problem at first we need to understand what just happened. And the answer in that particular case is quite simple: each time you create a subscription your subscription asks the stream for the last saved value (please, take into account, that not all streams will emit the last value for each new subscription, but in this sample I’m replicating the behavior of ngrx Store). This value emitted by the stream itself, it means that it should bypass all the transformations again, including our “lightweight operation”.

In Angular we create a subscription to an observable each time we use async pipe. So to fix this problem we will perform three actions:

  • Reduce the number of async pipes where it’s possible to reduce number of subscriptions;
  • Change ChangeDetectionStrategy from Default to OnPush to reduce number change detection cycles (actually the second bunch of calls is triggered by redundant change detection cycle);
  • Cache result of our transformation for further use by using publishReplay operator;
screenshot of fixed application

Here is the resulting code (I’ve marked lines with changes with comments):

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { of } from 'rxjs';
import { map, publishReplay, refCount } from 'rxjs/operators';
@Component({
selector: 'app-root',
template: '<app-tree [data]="data"></app-tree>',
// changed strategy
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
data = of({
// no changes
}).pipe(
map((data) => {
console.warn('Do some lightweight operation');
return data;
}),
// cached result of transformation
publishReplay(),
refCount()
);
}
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'app-tree',
template: `
<!– combined two async pipes into one –>
<ng-container *ngIf="data | async as data">
<div>{{data.title}}: {{data.value}}</div>
<ng-container *ngIf="data.items">
<app-tree *ngFor="let item of data.items; index as i" [data]="getSubtree(i)">
</app-tree>
</ng-container>
</ng-container>
`,
// changed strategy
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TreeComponent {
@Input() data: Observable<any>;
getSubtree(i: number) {
return this.data.pipe(
map((data) => data.items[i])
);
}
}

Also you can find the original source code on the GitHub (without fixes) and play with it: https://github.com/anton-gorbikov/angular-samples/tree/master/rxjs

conclusion

Work with streams and reactive programming contains a lot of unexpected behaviors and potential problems. I encourage you to dive really deep into reactive programming because despite the fact that it contains some surprises in the long term it’s very beneficial and proper usage can lead to more clean code and design.

If you still have questions feel free to contact me in the comments below.

Also if you find this material useful don’t forget to subscribe and share it with your colleagues! Thanks!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s