EmotionTechテックブログ

株式会社エモーションテックのProduct Teamのメンバーが、日々の取り組みや技術的なことを発信していくブログです。

実測!Angularテンプレートでのメソッド呼び出しはどれだけ負荷が高いのか?

はじめに

こんにちは!フロントエンドエンジニアの有馬です。

以前、コードレビューでこんな指摘を受けたことがあります。

「テンプレートでメソッドを呼び出すとパフォーマンスに影響が出る可能性があるので、Pipeを使いませんか?」

当時はその理由を深く理解していませんでしたが、実際に計測してみると、その指摘の裏にはパフォーマンスに関わる明確な理由があることがわかりました。

今回は、多くの開発者が一度は書いてしまいがちなアンチパターン「テンプレート内でのメソッド呼び出し」に焦点を当てます。これが実際にどれだけアプリケーションの負荷を高めるのかを計測し、解決策として「純粋パイプ (Pure Pipe)」が有効となることを、具体的なデータと共にお届けします。

検証対象:パフォーマンス問題を引き起こすコード

まずは、今回の検証対象となるサンプルアプリを見てみましょう。数百件のアイテムリストを表示し、各アイテムに対して少し重い計算処理を行うメソッドをテンプレートから呼び出しています。

app.component.ts

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

@Component({
  selector: 'app',
  templateUrl: './app.component.html',
})
export class AppComponent {
  items = Array.from({ length: 500 }, (_, i) => ({
    id: i,
    value: `Item ${i}`
  }));

  getHeavyCalculation(value: string) {
    // このログが何回表示されるかに注目!
    console.log(`Calculating for ${value}...`);
    
    // わざと重くした計算処理
    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += Math.sqrt(i);
    }
    return `${value} processed`;
  }
  
  // マウスを動かすたびに変更検知をトリガーするためのイベント
  onMouseMove() {
    // このメソッドの中身は空でOK
  }
}

app.component.html

<div (mousemove)="onMouseMove()">
  <h1>Item List</h1>
  <ul>
    @for (item of items; track item.id) {
    <li>{{ getHeavyCalculation(item.value) }}</li>
    }
  </ul>
</div>

このコードのキモは、@forの中でgetHeavyCalculation()メソッドを直接呼び出している点です。そして、わざとmousemoveイベントをバインドしていますが、これはユーザーが意図しない僅かな操作で、どれだけ無駄な処理が走ってしまうのかを体感してもらうためです。

Devtoolsでパフォーマンスを計測してみた (Before)

では、このアプリをブラウザで表示し、リストの上でマウスカーソルを少し動かしてみましょう。そして、その様子をAngular Devtoolsの Profiler で計測します。

衝撃の結果

  • コンソールのログが嵐のように流れる: マウスを少し動かしただけで、console.logが500件のアイテム分、何度も何度も繰り返し表示されます。1秒ほど動かすと6000件ものログが流れてきました。
  • Profilerが真っ赤になる: Profilerのフレームグラフは、非常に長いバーで埋め尽くされます。これは、一度の変更検知サイクルに非常に長い時間がかかっていることを示しています。各フレームで概ね1,000msかかっており、これでは60fps(1フレーム約16.7ms)の滑らかな描画は到底不可能です。

なぜこうなるのか?

これはAngularの変更検知(Change Detection)の仕組みに起因します。

Angularは、clickmousemoveなどのイベントが発生すると、「何かデータが変わったかもしれない」と判断し、コンポーネントツリーをチェックして画面を更新します。その際、テンプレート内に書かれたメソッドをすべて再評価します。

つまり、getHeavyCalculation()メソッドは、リストのアイテムが変更されたかどうかに関わらず、何らかのイベントが起きるたびに、全アイテム分(500回)呼び出されてしまうのです。これが、UIがカクつく原因となる深刻なパフォーマンス低下の正体です。

改善策:「純粋パイプ (Pure Pipe)」の利用

この問題を解決する最もシンプルな方法が純粋パイプ(Pure Pipe)です。

パイプはテンプレート内でデータを変換するための機能ですが、「純粋(Pure)」なパイプには重要な特性があります。

純粋パイプは、その入力値が変更された場合にのみ再実行される。

先の重い処理をパイプに移植して、この特性を利用しましょう。

heavy-calculation.pipe.ts

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

@Pipe({
  name: 'heavyCalculation',
  pure: true // これがデフォルトですが、純粋であることを明示
})
export class HeavyCalculationPipe implements PipeTransform {
  transform(value: string): string {
    // ロジックはコンポーネントに書かれたものと全く同じ
    console.log(`Pipe transforming ${value}...`);

    let sum = 0;
    for (let i = 0; i < 1000000; i++) {
      sum += Math.sqrt(i);
    }
    return `${value} processed`;
  }
}

(ちなみに、@Pipeデコレーターで pure: false を設定した「不純なパイプ」は、今回問題になったメソッド呼び出しと同様に、クリックやマウス移動などの変更検知のたびに毎回実行されます。)

そして、テンプレートの呼び出し箇所をこのパイプを使うように変更します。

app.component.html (改善後)

<div (mousemove)="onMouseMove()">
  <h1>Item List</h1>
  <ul>
    @for (item of items; track item.id) {
    <li>{{ item.value | heavyCalculation }}</li>
    }
  </ul>
</div>

Devtoolsで再計測! (After)

改善後のアプリで、もう一度同じようにリストの上でマウスを動かしてみましょう。

感動の結果

  • コンソールは沈黙: アプリの初回表示時にパイプが500回実行された後は、いくらマウスを動かしてもコンソールログは一切表示されません。
  • Profilerは穏やか: Profilerのフレームグラフは、非常に短い緑色のバーが並ぶだけです。各変更検知サイクルは1ms未満で完了しており、パフォーマンスへの影響は皆無と言えます。

入力値であるitem.valueが変化していないため、パイプは再実行されず、無駄な計算が一切行われなくなったのです。

パフォーマンスの変化が視覚的にわかるサンプルを用意したのでよかったら確認してみてください。

https://stackblitz.com/\~/github.com/hide-a1/angular-anti-pattern-sample

まとめ

今回の検証から、テンプレート内での安易なメソッド呼び出しは、深刻なパフォーマンス問題を引き起こすアンチパターンであることが明確にわかりました。

  • Before: マウスを動かすたびに、500件の重い処理が実行され、UIがカクつく。
  • After: 純粋パイプに置き換えることで、不要な再計算がゼロになり、パフォーマンスが劇的に改善。

データを加工して表示したい場合は、メソッドを呼び出すのではなく、なるべく純粋パイプを使いましょう。これは、Angularのパフォーマンスチューニングにおける、最も基本的で効果の高いテクニックの一つです。

(もちろん、非常に軽量なメソッドであれば問題にならないケースもあります。しかし、パイプを使う習慣をつけておくことが、意図せぬパフォーマンス低下を防ぐ最善策と言えるでしょう。)

あなたのテンプレートコードに{{ getHoge() }}のような記述はありませんか?ぜひ一度、そのメソッドの呼び出し回数を計測してみてください。思わぬパフォーマンスのボトルネックが発見できるかもしれません。