【Angular】Ivyってなに?

2020年3月3日

はじめに

V8からopt-inとして導入され、V9からすべてのアプリケーションでデフォルトで利用されるようになったIvyですが、そもそもIvyって何?という状況だったので整理の為いろんな記事読みながらまとめてみました。

TR;DR

  • Angular V9 からIvyがデフォルトになる。
  • Ivy利用すると、バンドルサイズが小さくなり、コンパイルが早くなり、生成されるコードが読みやすくなる。
  • entryComponentsが不要になる。(Ivyをオプトアウトするなら引き続き使う必要がある)

Ivyとは

IvyはAngularの第三世代のView Engineのことです。
View Engineとは、Angularコンポーネントをブラウザで使用できるようにコンパイルしてくれるコンパイラになります。
「第三世代」とついている通り、Angularの歴史の中で三つ目のViewEngineになります。
今まではRenderer、Renderer2というViewEngineがあったそうですが、V9からはIvyとなります。

Ivyで何が変わるの?

バンドルサイズ

フロントエンド開発の課題の一つとして、Webサイトのロード時間、というものがあると思います。
以下に早く読み込むのか、という点で、Ivyがバンドルサイズの縮小に貢献してくれます。
具体的にどれくらいバンドルサイズが削減できるのかというのは、アプリの規模などに影響されるので一概には言えないみたいです。
なんでバンドルサイズが削減できるのかというと、今までTreeShakingできなかった部分までできるようになったので、バンドルサイズを大きく減らせるようです。

コンパイル

Ivy ありなしの比較は、実際にコンパイルの結果を見るとわかりやすいです。
AngularCLIのプロジェクトなら以下のコマンドでコンパイル結果が出力されます。

node_modules/.bin/ngc -p tsconfig.app.json

IvyオプションのON,OFFはtsconfig.app.jsonを修正します。
V9にアップデートするとデフォルトでIvyはONになるので気にしないで大丈夫です。
OFFにする場合には、angularCompilerOptionsを追加します。

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ],
  "angularCompilerOptions": {
    "enableIvy": false
  }
}

IvyがOFF(今までの状態)でのコンパイル結果が以下の通りです。

IvyをONにすると以下のようになります。

上記の比較の通り、かなり変更されているのがわかると思うのですが、ここで注目しておきたいのが *.ngfactory.js というファイルが出力されなくなった点です。
これに関して、entryComponentsが非推奨になったことと合わせて説明していきます。

コーディング

コーディングという観点からみると、変更点は大きくありません。
V9からentryComponentsが非推奨になる、ということくらいだと思います。
Ivyを使う上では entryComponents を書く必要がなくなるそうです。
そもそも entryComponents とは何かというと、多くの場合で動的コンポーネントを実現するために利用されています。
V8以前において、Angular Material のDialogを使う際には以下のような記載が必須でした。

@NgModule({
  imports: [
    // ...
    MatDialogModule
  ],
  declarations: [
    AppComponent,
    ExampleDialogComponent
  ],
  // entryComponentsが必須
  entryComponents: [
    ExampleDialogComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

ですが、V9からはentryComponents部分が必要なくなります。
なので、以下のようになります。

@NgModule({
  imports: [
    // ...
    MatDialogModule
  ],
  declarations: [
    AppComponent,
    ExampleDialogComponent
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

もう少し踏み込んで話すと、Angularには二つのコンポーネントタイプが存在しています。
一つはテンプレートに含まれるコンポーネントで、
もう一つは命令的に(動的に)ロードされるコンポーネントです。
命令的に(動的に)ロードされるコンポーネントをエントリーコンポーネントと呼びます。

MVVMで言うと、コンポーネントはコントローラー/ビューモデルの一部で、テンプレートはビューを表現しています。

テンプレートに含まれるコンポーネント、というのがテンプレートファイル(.htmlファイル)に記述されている<app-root>とかのことで、命令的にロードされるコンポーネント( エントリーコンポーネント )はブートストラップする、またはルーティング定義で指定されたコンポーネントになります。

ブートストラップするコンポーネント(@NgModule.bootstrapにリストされたコンポーネント)と、ルーティング定義内のコンポーネントはエントリーコンポーネントとして自動で追加されるので基本的にはエントリーコンポーネントを明示的に設定する必要はないのですが、コンポーネントを動的にロードしたい(ボタンがクリックされたらこのコンポーネントをダイアログ表示したいとか)場合には、そのコンポーネント自体はルーティング定義にも @NgModule.bootstrap リストにも出てこないコンポーネントになるので、明示的に設定する必要があります。

わかりやすい実装例として、Angular Material のdialogを実装してみるとわかるかと思います。
https://material.angular.io/components/dialog/overview
※2020/2/18時点ではV9のリファレンスにも、entryComponentsに追加するような記載がありますが、V9からは不要なはずです。

なんでentryComponentsが不要になるの?そもそもなんで必要だったの?

この疑問を解消するためには、Tree shakingと、Angularの動的コンポーネントを生成する仕組みのComponentFactoryResolverというAPIへの理解が必要になります。

Tree shakingとは、特定のライブラリなどを指すものではなく、JavaScriptのコンテキストの中で一般的に使用される用語・手法のことになります。
Webpackなどでファイルをバンドルする際に、デッドコードなど、不要なコードがバンドルされるのを防ぐ仕組みのことをいいます。

ComponentFactoryResolberは、コンポーネントクラスから、Aotコンパイラが生成したComponentFactoryオブジェクトを返すものです。
ComponentFactoryオブジェクトは、ComponentFactory.create()メソッドを使用して、そのコンポーネントを作成してくれます。

V8以前のコンパイラでは、上記で見た通り、 ComponentFactoryResolber が生成したファイルを*.ngfactory.jsという形式で出力しています。
こういったファイルが出力された後に、Webpackによりファイルがバンドルされている最中にTreeShakingされるのですが、この*.ngfactory.jsに対して参照がない場合にはデッドコードと認識され、バンドルされなくなってしまいます。
今回サンプルでお見せしている画像は、app.component周りのものになるため、app.module.ts内でbootstrap(routingを利用する場合にはapp-routing.module.ts)により宣言されているため、 *.ngfactory.js に対する参照が保たれます。
ですが動的コンポーネントとなると、bootstrapやrouting定義で宣言されないため、そうはいかず、*.component.jsに対する参照は持つことができても、*.ngfactory.jsに対する参照を持つことができません。
そのため、そういった動的なコンポーネントに関してはentryComponentsとして定義するのがお約束になっていました。

ですが、Ivyでコンパイルしたものを見ると*.ngfactory.jsといったファイルは出力されていません。
ComponentFactory自体が*.component.jsに内包されるようになったからです。
そうなると*.component.jsへの参照があれば、自動で*.ngfactory.jsへの参照も保たれるので、entryComponentsへの定義追加が不要になる、ということです。

おわりに

今回Ivyに関していろいろな情報をまとめながら自分なりに理解した部分を記事にしてみました。
entryComponentsが非推奨になる点に関して、調べる前では漠然とIvyが入るから非推奨になるんだなぁとしか知らなかったですが、かなり明確になったかと思います。

参考記事

https://logmi.jp/tech/articles/302246
https://medium.com/angular-in-depth/all-you-need-to-know-about-ivy-the-new-angular-engine-9cde471f42cf
https://medium.com/@immanubhardwaj/renderer2-angular-view-engine-d872498be1e6
https://dev.to/angular-jp/entrycomponents-53mo