Skip to content
Angular Performance Optimization - No Call Expression

Angular Performance Optimization - No Call Expression

At OctoPerf we created a load testing web application using Angular for the frontend.

The application features IDE capabilities (like IntelliJ Idea or VSCode) to create load testing scripts:

OctoPerf IDE Screenshot

We faced several performance issues while creating such a complex web application. This blog post is the first of a series dedicated to common Angular performance issues and how to solve them.

Do YOU have custom Load Testing needs?
Rely on our Expertise

ESLint no-call-expression

If you are using ESLint for Angular you may have encountered the no-call-expression error. This error is raised when you call a function from a template expression.

For example, in the template bellow we call functionCall from the *ngIf structural directive:

<!-- eslint-disable @angular-eslint/template/no-call-expression -->
  <form [formGroup]='form' *ngIf='functionCall(form)'>
    <input [formControl]="formControl" type='text' />
  </form>
<!-- eslint-enable @angular-eslint/template/no-call-expression -->
The ESLint error can be disabled using the comment <!-- eslint-disable @angular-eslint/template/no-call-expression --> but you should really think twice before doing so!

Why calling functions in Angular template expressions is bad practice and can have huge performance impact on your application?

Prime number computation

To have a simpler use case than our load testing web application, we will create a simple input form that computes the next prime number.

Angular performance prime number input

There is no direct way to know if a given number is a prime number. So for a big input number, the computation can be time-consuming. This will allow us to check for performance issues in a simple Angular application.

Let's start by creating an Angular service that can tell if a number is prime and compute the next prime number for a given value:

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

@Injectable({
  providedIn: 'root'
})
export class PrimeService {

  public isPrime(number: number): boolean {
    if (number <= 1) {
      return false;
    }
    for (let i = 2; i < number; i++) {
      if (number % i == 0) {
        return false;
      }
    }
    return true;
  }

  public nextPrime(number: number): number {
    while (!this.isPrime(++number)) {
    }
    return number;
  }
}

Then we can create a component that contains a form and a button to compute the next value.

Call expression from template

This first version of our component called CallExpressionComponent has a FormControl named numberForm and three methods:

  • isPrime to know if the current numberForm value is a prime number,
  • computeNextPrimeNumber to update the numberForm value with the next prime number,
  • triggerChangeDetection that does nothing but triggers Angular change detection when called to simulate other interactions with our simple application.

CallExpressionComponent

import {Component} from '@angular/core';
import {PrimeService} from "../prime.service";
import {FormControl} from "@angular/forms";

@Component({
  selector: 'app-call-expression',
  templateUrl: './call-expression.component.html',
  styleUrls: ['./call-expression.component.scss']
})
export class CallExpressionComponent {

  readonly numberForm = new FormControl<number>(0, {nonNullable: true});

  constructor(private prime: PrimeService) {
  }

  isPrime(): boolean {
    console.time('isPrime');
    const isPrime = this.prime.isPrime(this.numberForm.value);
    console.timeEnd('isPrime');
    return isPrime;
  }

  computeNextPrimeNumber(): void {
    console.time('computeNextPrimeNumber');
    const number = this.numberForm.value;
    const next = this.prime.nextPrime(number);
    this.numberForm.setValue(next);
    console.timeEnd('computeNextPrimeNumber');
  }

  triggerChangeDetection(): void {
    // Nothing to do
  }
}

console.time and console.timeEnd are used to display messages in the browser console, letting us know how long it took to execute each function.

The associated HTML template is pretty simple with only a reactive input form, a button to call computeNextPrimeNumber() and text that tells the user if the input value is a prime number :

<input type="number" [formControl]="numberForm">
<button type="button" (click)="computeNextPrimeNumber()">Next</button>
<p (mousemove)="triggerChangeDetection()">
  is prime number ? {{isPrime()}}
</p>

(mousemove)="triggerChangeDetection()" does nothing but only triggers Angular change detection when the user moves the mouse cursor over the text.

In a real - more complex - application, Angular change detection can be triggered by any interaction with other external components.

Performance issue

When interacting with this form many calls are made to the isPrime function:

Angular performance console logs isPrime

That's inherent to the way Angular checks if changes were made to a displayed value: For each change detection cycle, the isPrime function is called twice to know if the value has changed and so if the display need to be updated!

With a small input number value, that's no big deal has the computation of the function call is fast. But let's put a big value and see what happens:

Angular performance prime number input big value

Now we can see that each one of the many function calls takes about 100-150 ms to compute. The application starts to slow down and the web browser even complains that the 'mousemove' handler took too much time:

Angular performance console logs on mouse over

Even though the triggerChangeDetection method does nothing, it still triggers Angular change detection, forcing calls to isPrime and slowing things down.

Angular Change Detection

That performance issue is quite insidious since there is no direct relation between the mousemove method call and the slow isPrime called in the template. In a real-life application the Angular detection cycle could be triggered by a completely unrelated component or directive, making it quite hard to debug.

That's precisely why you should avoid calling functions in Angular template expressions even if the computation is fast. The code may evolve with future specifications and releases, and you have no guaranty that it will remain fast in all cases.

Now let's see how we can improve performances of our test application by avoiding calls to the isPrime function from the template.

Solution 1: React to Form Update

The most straightforward way to handle this issue is to store the isPrime value in a variable.

CallNoExpressionComponent

Instead of calling the isPrime() method from the template, we simply display the isPrime variable value:

<input type="number" [formControl]="numberForm">
<button type="button" (click)="computeNextPrimeNumber()">Next</button>
<p (mousemove)="triggerChangeDetection()">is prime number ? {{isPrime}}</p>

The associated component must be updated to set the isPrime variable value each time the form control value changes. That's easy with reactive forms:

import {Component, OnDestroy} from '@angular/core';
import {FormControl} from "@angular/forms";
import {PrimeService} from "../prime.service";
import {Subject, takeUntil} from "rxjs";

@Component({
    selector: 'app-call-no-expression',
    templateUrl: './call-no-expression.component.html',
    styleUrls: ['./call-no-expression.component.scss']
})
export class CallNoExpressionComponent implements OnDestroy {

    private readonly destroyed = new Subject<boolean>();
    readonly numberForm = new FormControl<number>(0, {nonNullable: true});
    isPrime = false;

    constructor(private prime: PrimeService) {
        this.numberForm.valueChanges
            .pipe(takeUntil(this.destroyed))
            .subscribe(() => {
                console.time('isPrime');
                this.isPrime = this.prime.isPrime(this.numberForm.value);
                console.timeEnd('isPrime');
            });
    }

    computeNextPrimeNumber(): void {
        console.time('computeNextPrimeNumber');
        const number = this.numberForm.value;
        const next = this.prime.nextPrime(number);
        this.numberForm.setValue(next);
        console.timeEnd('computeNextPrimeNumber');
    }

    triggerChangeDetection(): void {
        // Nothing to do
    }

    ngOnDestroy(): void {
        this.destroyed.next(true);
        this.destroyed.complete();
    }

}

The functions computeNextPrimeNumber() and triggerChangeDetection() have not changed. The isPrime() method has been replaced by a variable whose value is updated in the numberForm.valueChanges listener.

Performance impact

With this version of the application, calls to our PrimeService are only made when the end-user interacts with the input or clicks on the Next button. No call is made when the Angular change detection is triggerred by a move mouse over the text!

That's a huge performance improvement. It can still be slow to compute the next prime value through:

Angular performance console logs on click

To improve user experience for this case we could add a loading state to the form, display an indeterminate progress bar, and even delay calls to the isPrime variable update using the RxJs debounceTime operator.

Careful with getters

In the first version of our component CallExpressionComponent, the method isPrime() could be replaced by a getter: get isPrime(): boolean {[...]}.

This would make it even harder to spot a performance issue because the HTML template would not show a method call:

<p (mousemove)="triggerChangeDetection()">is prime number ? {{isPrime}}</p>

But the getter isPrime would be called as many times as the isPrime() was, leading to the same performance problems.

Another - more elegant? - solution would be to use Angular pipes. Let's see how it is done!

Solution 2: Use Pipe Transform

Pure Angular pipes are called only when their input value changes.

IsPrimePipe transform

This prevents the tranform method from being called for each detection cycle:

import {Pipe, PipeTransform} from '@angular/core';
import {PrimeService} from "../prime.service";

@Pipe({
  name: 'isPrime',
  // pure: false => triggered by change detection
})
export class IsPrimePipe implements PipeTransform {

  constructor(private prime: PrimeService) {
  }

  transform(value: number): boolean {
    console.time('isPrime');
    const isPrime = this.prime.isPrime(value);
    console.timeEnd('isPrime');
    return isPrime;
  }

}

Note:

Setting the @Pipe parameter pure to false changes the update behavior: The transform method is called during each detection cycle to check if the output changed (just like it was done for the call to the isPrime() method).

CallPipeComponent

The component template now uses {{numberForm.value | isPrime}} to display the result of the isPrime() method instead of a direct call to the service:

<input type="number" [formControl]="numberForm">
<button type="button" (click)="computeNextPrimeNumber()">Next</button>
<p (mousemove)="triggerChangeDetection()">is prime number ? {{numberForm.value | isPrime}}</p>

So the isPrime() method can be removed from the component's typescript:

import { Component, OnInit } from '@angular/core';
import {FormControl} from "@angular/forms";
import {PrimeService} from "../prime.service";

@Component({
    selector: 'app-call-pipe',
    templateUrl: './call-pipe.component.html',
    styleUrls: ['./call-pipe.component.scss']
})
export class CallPipeComponent {

    readonly numberForm = new FormControl<number>(0, {nonNullable: true});

    constructor(private prime: PrimeService) {
    }

    computeNextPrimeNumber(): void {
        console.time('computeNextPrimeNumber');
        const number = this.numberForm.value;
        const next = this.prime.nextPrime(number);
        this.numberForm.setValue(next);
        console.timeEnd('computeNextPrimeNumber');
    }

    triggerChangeDetection(): void {
        // Nothing to do
    }
}

Another advantage of this solution is that it simplifies the component by extracting the logic of calling the PrimeService.isPrime() method into the Angular pipe.

This code is easier to read, to test, and to maintain.

Conclusion

We identified a potential performance issue caused by calling a method from the template of an Angular component.

Dangerous use cases are:

  • Calling an expression to display a value: {{methodCall()}},
  • Calling a method to set an input value: <app-my-component [inputValue]='methodCall()'></app-my-component.

These cases can lead to your whole Angular application being slowed down, without direct correlation between the user interaction and the cause of the performance issue. That make it really hard to debug!

There is no issue with calling a method to handle a user interaction: <button (click)="doSomething()"> unless the method called takes a long time to compute and a loading state must be implemented.

We saw two solutions to handle this problematic: using a variable in the component or using an external Pipe.

Another solution to degraded performances on Angular applications is to set the ChangeDetectionStrategy of the component to OnPush.

Want to become a super load tester?
Request a Demo