📘

Angularを実際にZonelessにしたプロセスとその効果

に公開
  • Zonelessにした場合のBundleサイズの削除効果は限定的
  • 本当のメリットは以下の通り
    • 必要最低限の変更検知にすることでのパフォーマンスの向上
    • 起動時のオーバーヘッド(Monkey Patchingによるコスト)の削減
    • デバッグ体験の向上 / より良いエコシステム互換性
  • Zonelessを目指そう

規模の小さいWebサイト(Angular Prerenderサイト)をZonelessにしたので、軽くまとめておきます。

Zonelessとは

Angularでは、デフォルトで zone.js を利用して「変更検知(Change Detection)」をトリガーしています。Zone.jsは、JavaScriptの非同期処理(setTimeout、Promise、XHRなど)をフックして、何かしらのイベントが終わった後にAngularに「変更があったかもよ!」と通知して、UIの再描画を促します。

けれど、Zone.jsでの変更検知は、不要な変更検知が走ってしまうことでのパフォーマンスの悪影響であったり、またZone.jsがペイロードサイズと起動時間の両方で、かなりのオーバーヘッドをもたらします。そこで、AngularチームはZone.jsを使わないAngular = Zonelessを目指して、実装を進めていました。

Zonelessにしたプロセス

ChangeDetectorRef.markForCheck APIに差し替えるのはとてつもなく手間というか、漏れがあった場合変更検知が走らないため私にとっては現実的ではありませんでした。そこで、変更検知が必要(厳密にはテンプレートバインディングしていないことがあるのでイコールではありませんが、readonlyではないプロパティ)を、すべてSignalsに置き換え、Signalsによる変更検知に差し替えるようにしました。詳しくは以下をご覧ください。

https://zenn.dev/rdlabo/articles/c6623c6ccc16dd

そのあとで、以下に沿って、 provideExperimentalZonelessChangeDetection APIをブートストラップに入れ、Zone.jsを削除しました。

https://angular.jp/guide/experimental/zoneless

実測値

https://concent-market.com/

Before: 352.51 kB / 102.63 kB

Initial chunk files   | Names                |  Raw size | Estimated transfer size
main-4P6REGVR.js      | main                 | 247.83 kB |                66.46 kB
...
                      | Initial total        | 352.51 kB |               102.63 kB

After: 318.21 kB / 91.27 kB

Initial chunk files  | Names                |  Raw size | Estimated transfer size
main-HCMXWMTQ.js     | main                 | 248.33 kB |                66.61 kB
...
                     | Initial total        | 318.21 kB |                91.27 kB

https://benaton.net/

Before: 1.47 MB / 352.99 kB

Initial chunk files   | Names                |  Raw size | Estimated transfer size
main-FDNOEG6I.js      | main                 |   1.35 MB |               315.58 kB
...
                      | Initial total        |   1.47 MB |               352.99 kB

After: 1.43 MB / 341.48 kB

Initial chunk files  | Names                |  Raw size | Estimated transfer size
main-HRTUBHRQ.js     | main                 |   1.35 MB |               315.60 kB
...
                     | Initial total        |   1.43 MB |               341.48 kB

考察

Browser bundlesのInitial bundleだけをまとめると、以下のようになります。benaton.netのもともとのサイズが大きいのは、Firebase Storeを内部で使っており、そのSDKがあるからですね。

concent-market.com Raw size Estimated transfer size
Before 352.51 kB 102.63 kB
After 318.21 kB 91.27 kB
benaton.net Raw size Estimated transfer size
Before 1.47 MB 352.99 kB
After 1.43 MB 341.48 kB

Zone.jsは単一ファイルなので、ドキュメントにあるようにZonelessはBundleサイズの削減をメインにしたものではないことがわかります。それよりは、以下の2点を注視すべきでしょう。

1. 必要最低限の変更検知にすることでのパフォーマンスの向上

Zone.jsは「きっと変更しているだろう」という予測に基づいて変更検知を行い、再レンダリングを行っていました。これは不要な再レンダリングも行われており、パフォーマンスの低下原因になりえるものです。
Zonelessにするためには、このZone.jsに任せっきりだった変更検知を手動(※ 私はSignal化したことで、Signalの値の評価に依存しました)することが必要です。このプロセスを通して、不要な再レンダリングというパフォーマンスの低下原因を取り除くことになり、アプリ全体のパフォーマンス向上につながります。

これは大きな規模のアプリであるほど如実ですね。まだZonelessには至っていないのですが(※使っている他のライブラリにZone.js依存があるため)、実際にマイグレーションを行うと明らかにパフォーマンスが改善されたと実感することができます。

2. 起動時のオーバーヘッド(Monkey Patchingによるコスト)の削減

Zone.jsは、 setTimeout , Promise , addEventListener などの標準の非同期APIを全部上書き(Monkey Patch)して、非同期処理の後にAngularの Change Detection を自動でトリガーするよういにします。このことで、アプリ起動時にPatch処理が行われ、また非同期のあとは都度AngularのChange Detectionを処理するので、これがオーバーヘッドになっていました。
Zonelessにすることで、このオーバーヘッドが取り除かれます。

すべての変更検知を手動化できていれば、オーバーヘッドをもつ必要はないのですみやかにZonelessへの移行がおすすめできます。

3. デバッグ体験の向上 / より良いエコシステム互換性

これに関しては私はまだ実感できていないのですが、Angularは以下のように書いています。

  • ZoneJSは、コードのデバッグをより困難にします。スタックトレースはZoneJSでは理解しにくくなります。また、コードがAngular Zoneの外部にあるために壊れた場合も理解しにくいです。
  • ZoneJSはブラウザAPIをパッチ適用することで動作しますが、すべての新しいブラウザAPIに対して自動的にパッチが適用されるわけではありません。

まとめ

AngularのZonelessへの作業の面白いところは、ZoneJSを取り外したら終了!というよりは、ZoneJSを取り外すプロセスでAngularの変更検知をより詳しく理解することができ、一歩進めるごとにパフォーマンスが改善していくことを実感できることだと思います。新規案件と不具合回収で追われていて、パフォーマンスまで取り組めていない人は、一度優先度を見直してみてもおもしろいのではないでしょうか。

それではまた。

Discussion