18
12

More than 3 years have passed since last update.

【Angular】なぜTemplateから関数を直接呼び出すとパフォーマンスの低下を起こすのか、とその対策

Last updated at Posted at 2020-02-11

はじめに

よくAngularのパフォーマンス改善で言われている「Templateからメソッドを直接呼んではいけない」はなぜなのか、をこちらの記事を参考に実際に試して確認、そしてその対策方法を共有する。

注意事項

今回はPipesをメインに紹介したいため、@Input()ChangeDetectionStrategy.OnPushを使った対策は割愛する。

今回使っていくソースコード

1~100までの数値とその数値が偶数かどうかを表示する。
数値の横のボタンがクリックされると、その数値を100倍にして更新する。

call-function-from-template.component.ts
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');
    }
}
call-function-from-template.component.html
<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>

こんな感じに表示される。
スクリーンショット 2020-02-11 16.50.31.png

Templateからメソッドを直接呼ぶとChangeDetectionのたびに実行されてしまう

見出しにある通り、なぜTemplateから直接メソッドを呼んでいけないかというとそのメソッドがChangeDetectionのたびに呼ばれてしまうからだ。

試しに「Button click Event」ボタンをクリックしてみると画像のように100個分の要素からメソッドが呼ばれてしまっている。
※なぜ2回呼ばれているのかはわからない…。時間があったら調べてみます。もし知っている方いたらコメントください…。

call-func.gif

ChanteDetectionStrategy.OnPushじゃ防げない?

NumberList@Input()を使って親から渡されているかつ、clickイベントが親コンポーネントで実装されていれば、ChangeDetectionStrategy.OnPushで防ぐことはできる。が、そのコンポーネント内だとChangeDetectionStrategy.OnPushは効かないため、この設定だけやれば防げるものではない。

対策: Pure Pipeを使う

AngularのPipesには Pure PipeImpure Pipe の2つが存在する。ちなみに デフォルトではPure Pipe の設定になっている。

example.pipe.ts
@Pipe({
  name: 'example',
  pure: true // ←ここで切り替える。デフォルトではtrue
})

Pure Pipeというのは、渡されたvalueがプリミティブであれば値の変更、オブジェクト(配列含む)であれば新しい参照が与えられたときだけtransform()が実行されるPipeのことだ。

まずは与えられた数値が偶数かどうか判定するPipeを作成する。
※実行されたかわかりやすいようにログも混ぜてある。

is-even.pipe.ts
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を使うように修正する。

call-function-from-template.component.html
<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のみだけ実行されていることが確認できる。
call-pipe-func.gif

さいごに

ChangeDetectionはいろんなところで出てくるんだなぁ…(雑)

参考

Why you should never use function calls in Angular template expressions

18
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
12