Angular: DON'T Call Functions Inside The Template Anymore

Why you Should NOT Call Functions Inside Angular Templates

By Maria Zayed

September 22nd, 2021

image

A template is a form of HTML that tells Angular how to render the component. Every component has its template, which defines its elements and how it should look. But one of the most frequent miss-using techniques is calling a function inside the template.

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

@Component({
  selector: 'app-performance-issue',
  template: `
    <div *ngFor="let name of filterNames()">{{ name }}</div>`,
})
export class PerformanceIssueComponent {

  names = ['Maria', 'Michel', 'Jack', 'John', 'Sam', 'Mila'];

  filterNames(): string[] {
    return this.names.filter(name => name.startsWith('M'));
  }
}

It's easy to get the calculated values in this way. But this can lead to a serious performance issue which we should be aware of.

The Problem

The core of the problem is related to the change detection mechanism, which Angular uses to detect the changed data and update the DOM's affected parts. However, Angular can't detect if there is any change in values from function calls. The fact is, all functions in templates will be re-executed in every change detection cycle for comparison.

For example, in the above code snippet, it's obvious that filterNames() is dependent on names. If there is no change in filterNames() there is no change in names as well. Even if there are no value changes during the change detection cycle, the filterNames() will still be called because the Angular engine can't detect this type of relation yet.

Let's take a practical look at the core of the problem.

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

@Component({
  selector: 'app-performance-issue',
  template: `
    <div *ngFor="let name of filterNames()">{{ name }}</div>
    <button (click)="onClick()">Click Me!</button>`,
})
export class PerformanceIssueComponent {

  names = ['Maria', 'Michel', 'Jack', 'John', 'Sam', 'Mila'];

  filterNames(): string[] {
    console.log("filterNames() called!");
    return this.names.filter(name => name.startsWith('M'));
  }

  onClick() {
    console.log("Button clicked!");
  }
}

Every time the button is clicked (which triggered a change detection cycle), the filterNames() function will always be re-executed, as shown in the screenshot below.

It is expected to have the function called twice per detection cycle in development mode. It is an Angular debugging feature. Here is a more detailed explanation

Having the functions called whenever the button is clicked sounds harmless as nobody will keep clicking it.

There are some features in Angular that trigger change detection very frequently in the background, such as binding to mousemove events. When we use these features in our application, the re-execution of functions will become a problem that we cannot ignore. For example, take a look at the code snippet below.

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

@Component({
  selector: 'app-performance-issue',
  template: `
    <div *ngFor="let name of filterNames()">{{ name }}</div>
    <div
        style="height: 300px; width: 300px; background-color: #2f487e;"
        (mousemove)="onMousemove()">
    </div>`,
})
export class PerformanceIssueComponent {

  names = ['Maria', 'Michel', 'Jack', 'John', 'Sam', 'Mila'];
  counter = 0;

  filterNames(): string[] {
    this.counter++
    console.log("filterNames() called! " + this.counter + " times!");
    return this.names.filter(name => name.startsWith('M'));
  }

  onMousemove() {
    console.log('Mouse moved');
  }
}

By moving the cursor around in the <div> with a mousemove binding, we will see the function got executed hundreds of times in a very short period.

The impact may not be significant at first with only several function calls. However, they stack up exponentially when more and more new components are added to the application, eventually eating up all the computation power and making the application slow.

Some may think of using getter methods to avoid function calls. But in fact, it doesn't work because the getter method is also a function call, which has the same problem.

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

@Component({
  selector: 'app-performance-issue',
  template: `
    <div *ngFor="let name of filteredNames">{{ name }}</div>
    <div
        style="height: 300px; width: 300px; background-color: #2f487e;"
        (mousemove)="onMousemove()">
    </div>`,
})
export class PerformanceIssueComponent {

  names = ['Maria', 'Michel', 'Jack', 'John', 'Sam', 'Mila'];
  counter = 0;

  get filteredNames(): string[] {
    this.counter++
    console.log("filterNames() called! " + this.counter + " times!");
    return this.names.filter(name => name.startsWith('M'));
  }

  onMousemove() {
    console.log('Mouse moved');
  }
}

The Solution

In fact, there is no perfect solution for this issue. Angular must re-execute all the functions inside the template during change detection, which is a limitation of its design. A possible workaround is to avoid template function calls with complicated computation. If we need to have some computed values, we should manage them ourselves manually rather than rely on Angular change detection.

Solution1 — using the setter function

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

@Component({
  selector: 'app-performance-issue',
  template: `
    <div *ngFor="let name of filteredNames">{{ name }}</div>`,
})
export class PerformanceIssueComponent {

  filteredNames = []

  @Input() set names(value: string[]) {
    this.filteredNames = value.filter(name => name.startsWith('M'));
  }
}

Solution2 — using ngOnChanges

import {Component, Input, OnChanges, SimpleChanges} from '@angular/core';

@Component({
  selector: 'app-performance-issue',
  template: `
    <div *ngFor="let name of filteredNames">{{ name }}</div>`,
})
export class PerformanceIssueComponent implements OnChanges{

  @Input() names = []
  filteredNames = []

  ngOnChanges(changes: SimpleChanges): void {
    if (changes.value) {
      this.filteredNames = this.names.filter(name => name.startsWith('M'));
    }
  }
}

Thanks for reading!

Any comments would be highly appreciated.

Happy Coding :D



Continue Learning