Angular and RxJS

A deep dive into async pipe

I have built a sample Angular application with a forked version of the async pipe. I have added console.log() statements labeled Async: throughout the forked version and left everything else unchanged. The components simulate various situations with the async pipe.

Download the source code.

We will cover several examples of how the async pipe is working at run-time, as well as practical applications of the async pipe for performant and maintainable code. If the async pipe is new to you, see here and here for a quick introduction.

Async pipe creation

intro.component.html

When an async pipe is created, it will start invokingtransform() on every change-detection cycle. On the first call to transform, a subscription will be created from the observable we passed in. If our observable emits a new value, the display value (this._latestValue) will be updated.

Note, obj is the observable we’ve passed in, also known as the source observable. this._obj is the saved state of our observable inside the async pipe. this._latestValue is the value we see in {{data}}, which we will refer to as our display/latest value.

async.pipe.ts
The initial output from observable$ | async

Practical: don’t create multiple async pipes for the same observable

For each async pipe created, a new redundant subscription will be created along with it. Each pipe’s transform method will be invoked on every change-detection cycle. Using as, we can reuse the latest value from our async pipe and maintain a single subscription. This will keep our application performant and maintainable.

clean.component.ts & dirty.component.ts
clean.component.ts

In clean.component.html, when observable$ emits a value, one change event will be fired and the transform method will be invoked once. In dirty.component.html, when observable$ emits a value, three change events will be fired and each async pipe will invoke the transform method three times. For three redundant pipes, the output looks like this:

dirty.component.html

Async pipe destruction

The async pipe will be destroyed when its container is removed from the DOM, or its parent component is destroyed. When the async pipe is destroyed, it performs any necessary clean-up, such as unsubscribing. At no point do we need to explicitly/manually call unsubscribe on the source observable’s subscription.

toggle.component.html
Toggling Create/Destroy of Async Container

We can verify that our subscriber is being cleaned up by running an allocation timeline in the Google Chrome Dev Tools. Starting with 51 subscribers, we toggle the component repeatedly. Once we stop, any subscribers we created while toggling have been released, as we are still at 51 subscribers. For more information on the allocation timeline, see here.

Allocation Timeline

Practical: async pipe combine

The RxJS operator combineLatest allows us to combine multiple observables into one observable that contains an array of all our values. We can map this array to an object and then access any of our display values on that object from the async pipe. [2]

combine.component.ts
combine.component.html

Note, the async pipe treats changes to any value inside allData$ as a single change to the object. This keeps our component performant with a single subscription that emits once on change detection.

Changing Values inside allData$

Async pipe reassignment and completion

Async pipe reassignment

If the source observable is reassigned, the async pipe will unsubscribe from the previous observable and subscribe to the new observable. This is essentially edge case subscription management.

async.pipe.ts
Reassigning observable$

Async pipe completion

If the observable is completed, emissions will stop and our latest value will still be displayed in the DOM. However, the async pipe will still invoke the transform method on every change detection cycle until it is destroyed.

Toggling Change-Detection After Completion

Practical: optimize with OnPush when applicable (advanced)

Async pipe invokes the transform method on every change-detection cycle because it is an impure pipe [3]. For every call to transform, the async pipe performs a check to see if the latest value has changed and returns early if it has not.

async.pipe.ts combine me

However, we can avoid these redundant checks altogether. The async pipe optimizes when its parent component is using ChangeDectionStrategy.OnPush. When the parent component uses this strategy, transform will only be called when the view has been marked for changes.

When a value emits from the async pipe’s source observable, it will mark the view to be checked for changes via markForCheck(). This means our async pipe will only tell our component it needs to re-render when its source observable emits a value.

async.pipe.ts

In other words, the async pipe enables us to take advantage of ChangeDetectionStrategy.OnPush, without having to explicitly/manually manage change detection.

We can validate this idea by simulating multiple change-detection cycles. I have added a button to our application to simulate change-detection by firing unrelated events in the DOM. We’ll start by testing ChangeDetectionStrategy.Default.

intro.component.ts
default strategy

Now we’ll change intro.component.ts to utilize the on-push change detection strategy. We can see that the transform method is no longer being invoked the additional ten times. Note, the view is still being updated in our application as expected.

intro.component.ts
on-push strategy
Latest Value from Async Pipe

Caution: while on-push can improve the performance of our components, it must be used judiciously and only when applicable. Using on-push incorrectly can result in bugs where theview is not updated as expected. [4]

Summary

  • Use a single async pipe per observable to avoid redundant subscriptions
  • Use the RxJS combineLatest operator when applicable to combine data into a single async pipe
  • Enable the on-push change detection strategy with the async pipe using caution and only when applicable

Resources / References

[A]: Introduction https://angular.io/guide/observables-in-angular#async-pipe

[B]: Introduction https://angular.io/guide/pipes

[1] Article on detecting memory leaks: https://itnext.io/angular-rxjs-detecting-memory-leaks-bdd312a070a0

[2] I saw this technique at ngConf, credit goes to Deborah Kurata. https://www.youtube.com/watch?v=Z76QlSpYcck

[3] Search for pure/impure pipes inside this guide for more information https://angular.io/guide/pipes

[4] https://angular.io/api/core/ChangeDetectionStrategy

[5] My code snippets are built in https://carbon.now.sh

[6] Thank you to my friends for proofreading and helping me edit