Best Practices for Performant Angular Applications - #Part 1
Babloo Kumar
Posted on January 30, 2021
This article outlines the practices we should generally use in angular applications to have highly performant and cleaner code base.
In this series of articles I'll be posting one practice at time.
Scope of this series article will be limited to Angular, Typescript, RxJs and @ngrx/store.
Today lets talk about observables, From coding aspect, it's very important to understand observables and how do we use it. If we use it correctly we can probably reduce the damage we do to the overall performance of the app and save lot of memory and CPU usage at run time.
Some of the best practices are mentioned below, and trust me these are not complicated at all, it's like developing good habit or bad habit and it will automatically be reflected in your day to day activities.
1. Subscribe in template
Avoid subscribing to observables from components and instead subscribe to the observables from the template.
Why it's important ?
async pipes unsubscribe themselves automatically and it makes the code simpler by eliminating the need to manually manage subscriptions. It also reduces the risk of accidentally forgetting to unsubscribe a subscription in the component, which would cause a memory leak. This risk can also be mitigated by using a lint rule to detect unsubscribed observables.
This also stops components from being stateful and introducing bugs where the data gets mutated outside of the subscription.
Before
// template
<p>{{ textToDisplay }}</p>
// component
meTheObservable
.pipe(
map(value => value.item),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
After
// template
<p>{{ textToDisplay$ | async }}</p>
// component
this.textToDisplay$ = meTheObservable
.pipe(
map(value => value.item)
);
2. Clean up subscriptions
When subscribing to observables, always make sure you unsubscribe from them appropriately by using operators like take, takeUntil, etc.
Why it's important ?
Failing to unsubscribe from observables will lead to unwanted memory leaks as the observable stream is left open, potentially even after a component has been destroyed / the user has navigated to another page.
Even better, make a lint rule for detecting observables that are not unsubscribed.
Before
meTheObservable
.pipe(
map(value => value.item)
)
.subscribe(item => this.textToDisplay = item);
After
Using takeUntil when you want to listen to the changes until another observable emits a value:
private _destroyed$ = new Subject();
public ngOnInit (): void {
meTheObservable
.pipe(
map(value => value.item)
// We want to listen to meTheObservable until the component is destroyed,
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
}
public ngOnDestroy (): void {
this._destroyed$.next();
this._destroyed$.complete();
}
Using a private subject like this is a pattern to manage unsubscribing many observables in the component.
Using take when you want only the first value emitted by the observable:
meTheObservable
.pipe(
map(value => value.item),
take(1),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
Note:
The usage of takeUntil with take here. This is to avoid memory leaks caused when the subscription hasn’t received a value before the component got destroyed. Without takeUntil here, the subscription would still hang around until it gets the first value, but since the component has already gotten destroyed, it will never get a value — leading to a memory leak.
3. Avoid having subscriptions inside subscriptions
Sometimes you may want values from more than one observable to perform an action. In this case, avoid subscribing to one observable in the subscribe block of another observable. Instead, use appropriate chaining operators. Chaining operators run on observables from the operator before them. Some chaining operators are: withLatestFrom, combineLatest, etc.
Before
firstObservable$.pipe(
take(1)
)
.subscribe(firstValue => {
secondObservable$.pipe(
take(1)
)
.subscribe(secondValue => {
console.log(`Combined values are: ${firstValue} & ${secondValue}`);
});
});
After
firstObservable$.pipe(
withLatestFrom(secondObservable$),
first()
)
.subscribe(([firstValue, secondValue]) => {
console.log(`Combined values are: ${firstValue} & ${secondValue}`);
});
Why it's important ?
Code feel/readability/complexity : Not using RxJs to its full extent, suggests developer is not familiar with the RxJs API surface area.
Performance: If the observables are cold, it will subscribe to firstObservable, wait for it to complete, THEN start the second observable’s work. If these were network requests it would show as synchronous.
Conclusion
Developing applications is a journey on the road not taken and there's always room to learn and improve the things. The optimizations techniques mentioned above are good place to start and applying these patters consistently will make you and your users happy with less buggy and performant application.
In the next part I would take another topic and discuss on it.
Posted on January 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.