DEV Community

Hamza Ahmed
Hamza Ahmed

Posted on

Angular Devs Beware: Are You Guilty of This Performance-Killing Practice?

Hello, fellow Angular enthusiasts! Today, we'll dive into an essential Angular optimization technique that can make your application run smoother and faster. As an Angular aficionado, I've seen countless developers get tripped up by direct function calls in templates. But fear not, we'll unravel this enigma together and have a bit of fun along the way. So, grab your favorite beverage, get comfy, and let's get cracking!

The Problem: Direct Function Calls in Templates

Picture this: you're building an Angular app with a simple search box that filters a list of items. In your component's template, you call a function to filter the items based on the search input. It seems harmless at first, but little do you know that this decision will come back to haunt you.

You see, direct function calls in Angular templates can lead to performance issues, especially in complex applications. When Angular runs change detection, it executes these functions every single time, even if nothing has changed. This can lead to a bloated, sluggish app that leaves your users scratching their heads in frustration.

That's where our hero comes in: properties and observables! These trusty sidekicks can help you manage your data more efficiently and give your app the performance boost it needs.

The Solution: Embrace Properties and Observables

We'll explore how to replace direct function calls in Angular templates with properties and observables. Along the way, we'll uncover some advanced techniques to keep your app running smoothly and efficiently.

Step 1: Set up your component

First, let's create a component that displays a list of items and includes a search input box. To keep things simple, we'll use an array of strings as our data source.

import { Component } from '@angular/core';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss']
})
export class SearchComponent {
  items = [
    'Apple',
    'Banana',
    'Orange',
    'Pineapple',
    'Strawberry',
    'Grape',
    'Mango',
    'Watermelon'
  ];

  searchText = '';
  filteredItems: string[] = [];

  constructor() {
    this.filteredItems = this.items;
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we've initialized the filteredItemsarray with the original items array. This will be our starting point for the filtering process.

Step 2: Create a method to filter the items

Now that our component is set up, we need a way to filter the items based on the search input. To do this, let's create a method called onSearchTextChange() that updates the filteredItems array when the search text changes.

onSearchTextChange() {
  const searchText = this.searchText.toLowerCase();
  this.filteredItems = this.items.filter(item =>
    item.toLowerCase().includes(searchText)
  );
}
Enter fullscreen mode Exit fullscreen mode

This method takes the current searchText, converts it to lowercase, and then filters the items based on whether they contain the search text.

Step 3: Update the template to avoid direct function calls

In our component's template, we'll start by binding the searchTextproperty to the input box using [(ngModel)]. This two-way binding ensures that our searchTextproperty stays in sync with the input value.

Next, we'll use the (input) event to call the onSearchTextChange() method we created earlier. This ensures that our method is only called when the search input actually changes, rather than on every change detection cycle.

Finally, we'll use the *ngFor directive to loop through the filteredItems array and display the results.

Here's the updated template:

<input
  [(ngModel)]="searchText"
  (input)="onSearchTextChange()"
  placeholder="Search items"
/>

<ul>
  <li *ngFor="let item of filteredItems">
    {{ item }}
  </li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Step 4: Embrace the power of observables

Now that we've set up our component to avoid direct function calls in the template, let's take it one step further and harness the power of observables. Observables are a fantastic tool in Angular's arsenal, enabling us to efficiently manage and react to data changes.

First, we'll need to import Subject and Observable from the rxjs library:

import { Subject, Observable } from 'rxjs';
Enter fullscreen mode Exit fullscreen mode

Next, create a Subject called searchTextChanged$ that will emit a new value whenever the search text changes:

typescript
private searchTextChanged$ = new Subject<string>();
Enter fullscreen mode Exit fullscreen mode

Now, update the onSearchTextChange() method to emit the current searchText value whenever it's called:

onSearchTextChange() {
  this.searchTextChanged$.next(this.searchText);
}
Enter fullscreen mode Exit fullscreen mode

Finally, we'll create an Observable called filteredItems$ that listens for changes to searchTextChanged$ and updates the filteredItems array accordingly. To do this, we'll use the debounceTimeand distinctUntilChangedoperators from rxjs, which help ensure that we're only processing new and unique search text values.

filteredItems$: Observable<string[]>;

constructor() {
  this.filteredItems$ = this.searchTextChanged$.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    map(searchText =>
      this.items.filter(item =>
        item.toLowerCase().includes(searchText.toLowerCase())
      )
    )
  );
}
Enter fullscreen mode Exit fullscreen mode

And that's it! With these changes, our search component now avoids direct function calls in the template, and efficiently updates the filteredItems array using observables.

Step 5: Use the async pipe for cleaner templates

While our search component works well, we can make our template even cleaner by using the async pipe. The async pipe subscribes to an observable and automatically updates the view whenever new data arrives. It also takes care of unsubscribing from the observable when the component is destroyed, preventing memory leaks.

Let's update our template to use the async pipe with the filteredItems$ observable:

<input
  [(ngModel)]="searchText"
  (input)="onSearchTextChange()"
  placeholder="Search items"
/>

<ul>
  <li *ngFor="let item of filteredItems$ | async">
    {{ item }}
  </li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Now our template is even cleaner and more readable!

Comparision of onSearchTextChange() for the direct and observable based approach

Direct function call in the template with onSearchTextChange(): When using a direct function call in the template, Angular runs the function during each change detection cycle. Even though the function is bound to the input event, Angular will still check the function call for changes each time it runs change detection. This can lead to poor performance, especially if the function is computationally expensive [in this case it is filtering in every change detection] or the application has frequent updates.
Observable-based onSearchTextChange(): In this case, the function onSearchTextChange() is called only when the input event is emitted, thanks to event binding. The function itself doesn't perform any filtering logic. Instead, it emits a new value to the searchTextChanged$ subject, which triggers the observable pipeline to perform the filtering. This ensures that filtering only occurs when the input value changes, leading to better performance compared to direct function calls in templates.

Now, let's explore some advanced techniques and best practices to make our Angular apps even more efficient and performant.


Step 6: Pure pipes for better performance

Angular provides a number of built-in pipes for transforming data in templates. By default, these pipes are "pure," meaning they only recompute their output when their input values change. This can lead to significant performance improvements, as Angular won't need to re-evaluate the pipe's output during every change detection cycle.

Consider creating custom pure pipes when you have a data transformation that is computationally expensive, but its output is solely determined by its input. Here's an example of a custom pure pipe that filters a list of items based on a search term:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filterItems',
  pure: true,
})
export class FilterItemsPipe implements PipeTransform {
  transform(items: string[], searchText: string): string[] {
    return items.filter(item =>
      item.toLowerCase().includes(searchText.toLowerCase())
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

To use our custom pipe in the template, simply replace the *ngFor directive with the following:

<li *ngFor="let item of items | filterItems: searchText">
  {{ item }}
</li>
Enter fullscreen mode Exit fullscreen mode

Now our filtering logic is encapsulated within a custom, pure pipe, making our component even more efficient!

Now, let's compare the observable-based onSearchTextChange() with the custom pure pipe solution.

Observable-based onSearchTextChange(): In this case, the function is called when the input event is emitted. The function itself doesn't perform any filtering logic. Instead, it emits a new value to the searchTextChanged$ subject, which triggers the observable pipeline to perform the filtering. This ensures that filtering only occurs when the input value changes, leading to better performance compared to direct function calls in templates.
Custom pure pipe solution: In this case, we use a custom pure pipe to handle the filtering logic. Pure pipes are more efficient because Angular only recomputes their output when their input values change. This means that the filtering logic is only executed when the input value or the list of items change. This approach eliminates the need for a separate method like onSearchTextChange() and encapsulates the filtering logic within the pipe.

Wrapping up

Congratulations! You've completed the article on avoiding direct function calls in Angular templates. We've covered everything from the basics of property binding and event handling to advanced techniques with observables and pure pipes. By implementing these best practices, you're well on your way to building high-performance Angular applications.

Remember, Angular is a vast and powerful framework with endless possibilities. Keep exploring, learning, and sharing your knowledge with the community. As a fellow Angular enthusiast, I can't wait to see what you create! Happy coding!

Top comments (4)

Collapse
 
ant_f_dev profile image
Anthony Fung

I came across a similar issue the other day. There was a direct function call in a template. The function returned a SafeUrl. While it would point to the same address every time, it was being detected as different because it was a different object being returned on each call of the template refreshing.

Collapse
 
xaberue profile image
Xavier Abelaira Rueda

Great tip! I've seen this too many times, even I did myself in the past.

However I would like to ask that given the two options, both valid afaik, which one would you recommend @zokizuan ?

Thanks in advance!

Collapse
 
zokizuan profile image
Hamza Ahmed

Sorry for the late reply, In my opinion I would prefer pure pipe if it's possible

Collapse
 
xsaints profile image
Info Comment hidden by post author - thread only accessible via permalink
Ahmed Hamza

Thanks for the Info

Some comments have been hidden by the post's author - find out more