DEV Community

Babloo Kumar
Babloo Kumar

Posted on

Best Practices for Performant Angular Applications - #Part 1

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);
Enter fullscreen mode Exit fullscreen mode

After

// template
<p>{{ textToDisplay$ | async }}</p>
// component
this.textToDisplay$ = meTheObservable
    .pipe(
       map(value => value.item)
     );
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
}

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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}`);
    });
});

Enter fullscreen mode Exit fullscreen mode

After

firstObservable$.pipe(
    withLatestFrom(secondObservable$),
    first()
)
.subscribe(([firstValue, secondValue]) => {
    console.log(`Combined values are: ${firstValue} & ${secondValue}`);
});

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)