The async
pipe is a powerful feature in Angular that helps manage subscriptions automatically. However, improper usage can lead to performance bottlenecks. In this article, we'll explore a common mistake developers make when using the async
pipe and how to avoid it.
The Problem: Unnecessary Subscriptions Inside Loops
Consider the following problematic code:
@for (item of items; track item.id) {
<user-item [user]="user$ | async" [item]="item"></user-item>
}
At first glance, this might seem fine. However, let's examine how the async
pipe works internally.
How the Async Pipe Works
When you pass an observable to the async
pipe, Angular creates a subscription:
this._subscribe(obj);
Here's how the transform
method is implemented:
transform<T>(obj: Observable<T> | Subscribable<T> | Promise<T> | null | undefined): T | null {
if (!this._obj) {
if (obj) {
try {
this.markForCheckOnValueUpdate = false;
this._subscribe(obj); // Creates a subscription
} finally {
this.markForCheckOnValueUpdate = true;
}
}
return this._latestValue;
}
if (obj !== this._obj) {
this._dispose();
return this.transform(obj);
}
return this._latestValue;
}
The subscription is assigned to _subscription
:
private _subscribe(obj: Subscribable<any> | Promise<any> | EventEmitter<any>): void {
this._obj = obj;
this._strategy = this._selectStrategy(obj);
this._subscription = this._strategy.createSubscription(
obj,
(value: Object) => this._updateLatestValue(obj, value),
(e) => this.applicationErrorHandler(e)
);
}
Each time the async
pipe is used, it creates a new subscription and updates the view:
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
if (this.markForCheckOnValueUpdate) {
this._ref?.markForCheck(); // Triggers change detection
}
}
}
Why is This a Problem?
Using the async
pipe inside a loop means that a new subscription is created for each iteration, leading to multiple unnecessary change detection triggers. This can severely impact performance.
The Solution: Using Async Pipe Outside the Loop
Instead of using the async
pipe inside the loop, extract its value beforehand using *ngIf
, the @if
directive, or the let
syntax in Angular's newer syntax:
Correct Approach 1: Using Let Syntax
let user = users$ | async;
@for (item of items; track item.id) {
<user-item [user]="user" [item]="item"></user-item>
}
Correct Approach 2: Using *ngIf
<ng-container *ngIf="users$ | async as user">
<ng-container *ngFor="let item of items; trackBy: trackById">
<user-item [user]="user" [item]="item"></user-item>
</ng-container>
</ng-container>
trackById(index: number, item: any): any {
return item.id;
}
Conclusion
Using the async
pipe correctly can significantly improve performance in Angular applications. Avoid placing it inside loops and instead extract the observable value beforehand to prevent unnecessary subscriptions and change detection cycles. By following these best practices, you can ensure your application remains efficient and responsive.
For more information, check out the Angular async pipe implementation on GitHub.
Happy coding!
Top comments (0)