はじめに
よくAngularのパフォーマンス改善で言われている「Templateからメソッドを直接呼んではいけない」はなぜなのか、をこちらの記事を参考に実際に試して確認、そしてその対策方法を共有する。
注意事項
今回はPipes
をメインに紹介したいため、@Input()
とChangeDetectionStrategy.OnPush
を使った対策は割愛する。
今回使っていくソースコード
1~100までの数値とその数値が偶数かどうかを表示する。
数値の横のボタンがクリックされると、その数値を100倍にして更新する。
import { Component, DoCheck } from '@angular/core';
@Component({
selector: 'app-call-function-from-template',
templateUrl: './call-function-from-template.component.html',
styleUrls: ['./call-function-from-template.component.scss'],
})
export class CallFunctionFromTemplateComponent implements DoCheck {
public numberList = Array.from({ length: 100 }).map((_, i) => i + 1);
constructor() {}
// ChangeDetectionを確認
ngDoCheck(): void {
console.log('CallFunctionFromTemplateComponent-DoCheck');
}
// その数値が偶数であればTrue, 奇数であればFalseを表示する。
// 今回Templateから呼ばれるメソッド
public isEven(n: number) {
console.log('Function isEvent Called');
return n % 2 === 0 ? 'True' : 'False';
}
// その数値を100倍して表示する
public changeNumber(n: number, index: number) {
this.numberList[index] = n * 100;
}
// ChangeDetectionを発火させるようのclickイベント
public checkChangeDetect() {
console.log('click event');
}
}
<p>call-function-from-template works!</p>
<div>
<button (click)="checkChangeDetect()">Button click Event</button>
</div>
<ul>
<li *ngFor="let number of numberList; let i = index;">
<div>
Number: {{number}}, isEven: {{isEven(number)}} <button (click)="changeNumber(number, i)">Change Number</button>
</div>
</li>
</ul>
Templateからメソッドを直接呼ぶとChangeDetectionのたびに実行されてしまう
見出しにある通り、なぜTemplateから直接メソッドを呼んでいけないかというとそのメソッドがChangeDetectionのたびに呼ばれてしまうからだ。
試しに「Button click Event」ボタンをクリックしてみると画像のように100個分の要素からメソッドが呼ばれてしまっている。
※なぜ2回呼ばれているのかはわからない…。時間があったら調べてみます。もし知っている方いたらコメントください…。
ChanteDetectionStrategy.OnPush
じゃ防げない?
NumberList
が@Input()
を使って親から渡されているかつ、clickイベントが親コンポーネントで実装されていれば、ChangeDetectionStrategy.OnPush
で防ぐことはできる。が、そのコンポーネント内だとChangeDetectionStrategy.OnPush
は効かないため、この設定だけやれば防げるものではない。
対策: Pure Pipeを使う
AngularのPipesには Pure Pipe と Impure Pipe の2つが存在する。ちなみに デフォルトではPure Pipe の設定になっている。
@Pipe({
name: 'example',
pure: true // ←ここで切り替える。デフォルトではtrue
})
Pure Pipeというのは、渡されたvalue
がプリミティブであれば値の変更、オブジェクト(配列含む)であれば新しい参照が与えられたときだけtransform()
が実行されるPipeのことだ。
まずは与えられた数値が偶数かどうか判定するPipeを作成する。
※実行されたかわかりやすいようにログも混ぜてある。
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'isEven',
})
export class IsEvenPipe implements PipeTransform {
transform(n: number): string {
console.log(`Pipe Function isEvent Called, Number: ${n}`);
return n % 2 === 0 ? 'True' : 'False';
}
}
またさっきのテンプレートをPipeを使うように修正する。
<p>call-function-from-template works!</p>
<div>
<button (click)="checkChangeDetect()">Button click Event</button>
</div>
<ul>
<li *ngFor="let number of numberList; let i = index;">
- <div>
- Number: {{number}}, isEven: {{isEven(number)}} <button (click)="changeNumber(number, i)">Change Number</button>
- </div>
+ <div>
+ Number: {{number}}, isEven: {{number | isEven}} <button (click)="changeNumber(number, i)">Change Number</button>
+ </div>
</li>
</ul>
これで実行してみると、ChangeDetection時にPipeの関数が呼び出されず、値が変更されたPipeのみだけ実行されていることが確認できる。
さいごに
ChangeDetectionはいろんなところで出てくるんだなぁ…(雑)
参考
Why you should never use function calls in Angular template expressions