Angular アプリのパフォーマンスを改善する方法

公開: 2020-04-10

最高のフロントエンド フレームワークについて語るとき、Angular を言及しないわけにはいきません。 ただし、プログラマーがそれを学習して賢く使用するには、多大な努力が必要です。 残念ながら、Angular の経験がない開発者がその機能の一部を非効率な方法で使用するリスクがあります。

フロントエンド開発者として常に取り組む必要がある多くのことの 1 つは、アプリのパフォーマンスです。 私の過去のプロジェクトの大部分は、拡張と開発が続けられている大規模なエンタープライズ アプリケーションに焦点を当てていました。 ここではフロントエンド フレームワークが非常に役立ちますが、正しく合理的に使用することが重要です。

Angular アプリケーションのパフォーマンスを即座に向上させるのに役立つ、最も一般的なパフォーマンス向上戦略とヒントの簡単なリストを用意しました。 ここでのヒントはすべてバージョン 8 の Angular に適用されることに注意してください。

ChangeDetectionStrategy と ChangeDetectorRef

変更検出 (CD)は、データの変更を検出して自動的に対応するための Angular のメカニズムです。 標準的なアプリケーションの状態変化の基本的な種類をリストできます。

  • イベント
  • HTTP リクエスト
  • タイマー

これらは非同期の相互作用です。 問題は、何らかの対話 (クリック、間隔、http 要求など) が発生し、アプリケーションの状態を更新する必要があることを Angular がどのように知るかということです。

答えはngZone です。これは基本的に、非同期の相互作用を追跡するための複雑なシステムです。 すべての操作が ngZone によって登録されている場合、Angular はいくつかの変更にいつ反応するかを知っています。 しかし、何が変更されたのか正確にはわからず、最初の深さの順序ですべてのコンポーネントをチェックする変更検出メカニズムを起動します。

Angular アプリの各コンポーネントには独自の Change Detector があり、Change Detection が起動されたときにこのコンポーネントがどのように動作するかを定義します。 Angular が Change Detection を起動すると、すべてのコンポーネントがチェックされ、そのビュー (DOM) がデフォルトで再レンダリングされる場合があります。

これは、ChangeDetectionStrategy.OnPush を使用して回避できます。

 @成分({
  セレクター: 'foobar',
  templateUrl: './foobar.component.html',
  styleUrls: ['./foobar.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
}))

上記のコード例からわかるように、コンポーネントのデコレーターにパラメーターを追加する必要があります。 しかし、この新しい変更検出戦略は実際にどのように機能するのでしょうか?

この戦略は、特定のコンポーネントがその @Inputs() のみに依存することを Angular に伝えます。 また、すべてのコンポーネント @Inputs() は不変オブジェクトのように動作します (たとえば、参照を変更せずにオブジェクトの @Input() 内のプロパティのみを変更すると、このコンポーネントはチェックされません)。 これは、多くの不要なチェックが省略され、アプリのパフォーマンスが向上することを意味します。

ChangeDetectionStrategy.OnPush を持つコンポーネントは、次の場合にのみチェックされます。

  • @Input() 参照が変更されます
  • コンポーネントのテンプレートまたはその子の 1 つでイベントがトリガーされます。
  • コンポーネント内の Observable がイベントをトリガーします
  • CDは ChangeDetectorRef サービスを使用して手動で実行されます
  • 非同期パイプがビューで使用されます (非同期パイプは、変更をチェックするコンポーネントをマークします。ソース ストリームが新しい値を発行すると、このコンポーネントがチェックされます)。

上記のいずれも発生しない場合、特定のコンポーネント内で ChangeDetectionStrategy.OnPush を使用すると、そのコンポーネントとすべてのネストされたコンポーネントが CD の起動後にチェックされなくなります。

幸いなことに、ChangeDetectorRef サービスを使用することで、データ変更への対応を完全に制御できます。 タイムアウト、リクエスト、サブスクリプションのコールバック内で ChangeDetectionStrategy.OnPush を使用すると、本当に必要な場合は CD を手動で起動する必要があることを覚えておく必要があります。

 カウンター = 0;

コンストラクター (プライベート changeDetectorRef: ChangeDetectorRef) {}

ngOnInit() {
  setTimeout(() => {
    this.counter += 1000;
    this.changeDetectorRef.detectChanges();
  }、1000);
}

上記のように、タイムアウト関数内で this.changeDetectorRef.detectChanges() を呼び出すことにより、 CDを手動で強制できます。 カウンターが何らかの方法でテンプレート内で使用されている場合、その値は更新されます。

このセクションの最後のヒントは、特定のコンポーネントのCDを永続的に無効にすることです。 静的コンポーネントがあり、その状態を変更しないことが確実な場合は、 CDを永久に無効にすることができます。

 this.changeDetectorRef.detach()

このコードは、ngAfterViewInit() または ngAfterViewChecked() ライフサイクル メソッド内で実行して、データの更新を無効にする前にビューが正しくレンダリングされていることを確認する必要があります。 このコンポーネントは、 detectChanges() を手動でトリガーしない限り、 CD中にチェックされなくなります。

テンプレートの関数呼び出しとゲッター

テンプレート内で関数呼び出しを使用すると、Change Detector が実行されるたびにこの関数が実行されます。 ゲッターでも同じ状況が発生します。 可能であれば、これを避けるように努めるべきです。 ほとんどの場合、 CDを実行するたびにコンポーネントのテンプレート内で関数を実行する必要はありません。 その代わりに、純粋なパイプを使用できます。

ピュアパイプ

純粋なパイプは、入力のみに依存する出力を持つ一種のパイプであり、副作用はありません。 幸いなことに、Angular のすべてのパイプはデフォルトで純粋です。

 @パイプ({
    名前: '大文字',
    純粋:本当
}))

しかし、pure: false を使用したパイプの使用を避ける必要があるのはなぜでしょうか? 答えはまた変更検出です。 ピュアではないパイプは CD の実行ごとに実行されますが、これはほとんどの場合必要ではなく、アプリのパフォーマンスを低下させます。 純粋なパイプに変更できる関数の例を次に示します。

 変換 (値: 文字列、制限 = 60、省略記号 = '...') {
  if (!value || value.length <= limit) {
    戻り値;
  }
  const numberOfVisibleCharacters = value.substr(0, limit).lastIndexOf(' ');
  return `${value.substr(0, numberOfVisibleCharacters)}${ellipsis}`;
}

ビューを見てみましょう:

 <p class="description">truncate(テキスト, 30)</p>

上記のコードは、純粋な関数を表しています。副作用はなく、出力は入力のみに依存します。 この場合、この関数を単純に純粋な pipeに置き換えることができます:

 @パイプ({
  名前: '切り捨て',
  純粋:本当
}))
export クラス TruncatePipe は PipeTransform を実装します {
  変換 (値: 文字列、制限 = 60、省略記号 = '...') {
    ...
  }
}

最後に、このビューでは、テキストが変更された場合にのみ実行されるコードを、 Change Detectionとは別に取得します。

 <p class="description">{{ テキスト | 切り捨て: 30 }}</p>

モジュールの遅延読み込みとプリロード

アプリケーションに複数のページがある場合、プロジェクトの論理部分ごとにモジュールを作成することを検討する必要があります。特に、モジュールの遅延読み込みは特に重要です。 シンプルな Angular ルーター コードを考えてみましょう。

 const ルート: ルート = [
  {
    道: ''、
    コンポーネント: HomeComponent
  }、
  {
    パス: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule)
  }、
  {
    パス: 'バー',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule)
  }
]
@NgModule({
  エクスポート: [RouterModule],
  インポート: [RouterModule.forRoot(ルート)]
}))
クラス AppRoutingModule {}

上記の例では、ユーザーが特定のルート(foo または bar) に入ろうとしたときにのみ、すべてのアセットを含む fooModule がロードされることがわかります。 Angular は、このモジュールの個別のチャンクも生成します。 遅延読み込みにより、初期負荷が軽減されます。

さらに最適化を行うことができます。 アプリがバックグラウンドでモジュールをロードするようにしたいとしましょう。 この場合、preloadingStrategy を使用できます。 Angular にはデフォルトで 2 種類の preloadingStrategy があります。

  • プリロードなし
  • PreloadAllModules

上記のコードでは、デフォルトで NoPreloading 戦略が使用されています。 アプリは、ユーザーの要求によって特定のモジュールの読み込みを開始します (ユーザーが特定のルートを見たい場合)。 これは、ルーターに追加の構成を追加することで変更できます。

 @NgModule({
  エクスポート: [RouterModule],
  インポート: [RouterModule.forRoot(routes, {
       preloadingStrategy: PreloadAllModules
  }]
}))
クラス AppRoutingModule {}

この構成により、現在のルートができるだけ早く表示され、その後、アプリケーションはバックグラウンドで他のモジュールをロードしようとします。 賢いですね。 しかし、それだけではありません。 このソリューションがニーズに合わない場合は、独自のカスタム戦略を作成するだけです。

BarModule など、選択したモジュールのみをプリロードするとします。 これは、データ フィールドに追加のフィールドを追加することで示されます。

 const ルート: ルート = [
  {
    道: ''、
    コンポーネント: HomeComponent
    データ: { プリロード: false }
  }、
  {
    パス: 'foo',
    loadChildren: ()=> import("./foo/foo.module").then(m => m.FooModule),
    データ: { プリロード: false }
  }、
  {
    パス: 'バー',
    loadChildren: ()=> import("./bar/bar.module").then(m => m.BarModule),
    データ: {プリロード: true }
  }
]

次に、カスタムのプリロード関数を作成する必要があります。

 @注入可能()
export クラス CustomPreloadingStrategy は PreloadingStrategy を実装します {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    route.data && route.data.preload を返しますか? load() : of(null);
  }
}

そしてそれを preloadingStrategy として設定します:

 @NgModule({
  エクスポート: [RouterModule],
  インポート: [RouterModule.forRoot(routes, {
       preloadingStrategy: CustomPreloadingStrategy
  }]
}))
クラス AppRoutingModule {}

今のところ、param { data: { preload: true } } を持つルートのみがプリロードされます。 残りのルートは、NoPreloading が設定されているように動作します。

カスタム preloadingStrategy は @Injectable() であるため、必要に応じて内部にいくつかのサービスを注入し、他の方法で preloadingStrategy をカスタマイズできることを意味します。
ブラウザーの開発者ツールを使用して、preloadingStrategy を使用する場合と使用しない場合の初期読み込み時間を等しくすることで、パフォーマンスの向上を調べることができます。 また、ネットワーク タブを見て、他のルートのチャンクがバックグラウンドで読み込まれていることを確認できますが、ユーザーは遅延なく現在のページを表示できます。

trackBy 関数

*ngFor を使用するほとんどの Angular アプリは、テンプレート内にリストされたアイテムを反復処理すると想定できます。 繰り返しリストも編集可能な場合、trackBy は絶対に必要です。

 <ul>
  <tr *ngFor="let product of products; trackBy: trackByProductId">
    <td>{{ product.title }}</td>
  </tr>
</ul>

trackByProductId(インデックス:番号, 商品:商品) {
  product.id を返します。
}

trackBy 関数を使用することにより、Angular はコレクションのどの要素が変更されたかを (指定された識別子によって) 追跡し、これらの特定の要素のみを再レンダリングすることができます。 trackBy を省略すると、リスト全体が再ロードされ、DOM でリソースを大量に消費する操作になる可能性があります。

事前 (AOT) コンパイル

Angular のドキュメントについて:

(…) Angular が提供するコンポーネントとテンプレートは、ブラウザーが直接理解することはできません。Angular アプリケーションをブラウザーで実行するには、コンパイル プロセスが必要です

Angular は、次の 2 種類のコンパイルを提供します。

  • Just-in-Time (JIT) – 実行時にブラウザーでアプリをコンパイルします
  • Ahead-of-Time (AOT) – ビルド時にアプリをコンパイルします

開発用途では、 JITコンパイルが開発者のニーズをカバーする必要があります。 それにもかかわらず、本番ビルドではAOTを確実に使用する必要があります。 angular.json ファイル内のaotフラグが true に設定されていることを確認する必要があります。 このようなソリューションの最も重要な利点には、レンダリングの高速化、非同期リクエストの減少、フレームワークのダウンロード サイズの縮小、およびセキュリティの強化が含まれます。

概要

アプリケーションのパフォーマンスは、プロジェクトの開発と保守の両方の段階で留意する必要があるものです。 ただし、考えられる解決策を自分で探すのは、時間と労力の両方がかかる場合があります。 これらのよくある間違いをチェックし、開発プロセス中にそれらを念頭に置くことは、すぐに Angular アプリのパフォーマンスを改善するのに役立つだけでなく、将来の失敗を避けるのにも役立ちます.

製品アイコンのリリース

Angular プロジェクトで Miquido を信頼してください

お問い合わせ

Miquido でアプリを開発したいですか?

Angular アプリでビジネスを後押しすることを考えていますか? お問い合わせいただき、Angular アプリ開発サービスをお選びください。