👌

Ionic AngularアプリのZone-basedとZonelessのパフォーマンス比較と考察

に公開

以下の記事で規模の小さいWebサイトのZoneless化をご紹介しました。

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

しかしながら、ペライチでのWebサイトでのZoneless化では、パフォーマンス改善やオーバーヘッド削減効果は限定的です。というか、差を感じることはできません。そこで、中規模程度のIonic AngularアプリをZoneless化してみました。

Zone-basedとZonelessのパフォーマンス比較

前提条件

Ionic AngularはZone.js依存のライブラリ

npmパッケージ @ionic/angular は、peerDependenciesに zone.js があります。

https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/package.json#L60C6-L60C13

そのため、Zonelessにして、プロジェクトからZone.jsを削除しても、Zone.js自体は依存関係上、node_modulesにはいます。ですので、Bundleサイズ削減効果に関してはZoneless化は一切の影響はありません。

ただ、内部でZone.jsに依存しているかといえば、調べた限りでは runOutsideAngular メソッドで利用してるだけでした。

https://github.com/search?q=repo%3Aionic-team%2Fionic-framework+this.z.&type=code

Ionic Angularは、Zoneless未対応

Angular自体はv16以降でZoneless(provideExperimentalZonelessChangeDetection)を実験的にサポートしていますが、Ionic AngularはZone.js前提で設計されているため、公式サポートは現時点でありません。

また、 https://ionicframework.com/docs/angular/lifecycle で、OnPush戦略についてこう書かれています。

Components that use ion-nav or ion-router-outlet should not use the OnPush change detection strategy. Doing so will prevent lifecycle hooks such as ngOnInit from firing. Additionally, asynchronous state changes may not render properly.

ion-nav または ion-router-outlet を使用するコンポーネントは、 OnPush 変更検出方式を使用しないでください。OnPush を使うことで、 ngOnInit などのライフサイクル・フックが実行されなくなる可能性があります。また、非同期状態の変更は正しくレンダリングされない場合があります。

Zonelessどころか、一部コンポーネントを含むならOnPush戦略もしないようにと書かれています。笑
なのですが、Zone.jsへの依存が薄いのでもしかしてとZoneless化したところ、ion-router-outletion-nav を使ったモーダルも正常に動作しているのを確認しています。

Note: 実験中バージョンとして一部のユーザにテスト運用を依頼していますが、現在のところ不具合は見つかっていません

実際に多くのユーザに使ってもらっているアプリをベースにZoneless化しているので、一通りのパターンも実装されているとは思いますが、公式ではZoneless対応は発表しておらず、この記事を参考にZoneless化しても正常に動く保証はありません。また、今後のIonicやAngularのバージョンアップで動作が変わる可能性もありますので、十分なテストが必要です。

同一環境でホスティング

Angularのプロダクションビルドで、かつ実運用に近づけるため、以下で比較用にホスティングしました。

Firebase Hostingで、Zonelessは、Zone-basedから以下のように変更したものです。

  • ion-navion-router-outlet を使ってるコンポーネントもOnPush戦略に変更
  • provideExperimentalZonelessChangeDetection APIをインポート
  • Zone.jsをプロダクトから削除

これ以外の差はありませんので、純粋に比較が可能です。なお、両方バックエンド(とデータベース)は本番用につながっています。

計測結果

定点観測ではなく、ローカルでLighthouse(ストットリングのシュミレーション)を使って比較しています。10回実行して10回とも同じ傾向がでていることを確認したあと、交互に3回ずつ追加で記事執筆用に実行しており、計測結果はその最後の3回です。ログインページはコンポーネント数も少なく、差はほとんどでませんので、ログインして表示される、アイテム一覧ページを対象にしました。Virtual Scrollを使っており、子コンポーネントが複数あるため、Zone-basedとZonelessで差が期待できたためです。

以下の通りです。正直なところ、誤差レベルだと思ってたので、あまりに差が如実で驚きました。

Zone-based: https://zone-based.winecode.app/

zone-based.png

1回目 2回目 3回目
First Contentful Paint 7.6 秒 8.6 秒 8.6 秒
Largest Contentful Paint 12.9 秒 12.8 秒 12.8 秒
Total Blocking Time 980 ミリ秒 1,110 ミリ秒 850 ミリ秒
Cumulative Layout Shift 0.057 0.001 0.057
Speed Index 8.1 秒 8.6 秒 8.6 秒

Zoneless: https://zoneless.winecode.app/

zoneless.png

1回目 2回目 3回目
First Contentful Paint 6.4 秒 4.9 秒 6.5 秒
Largest Contentful Paint 7.9 秒 7.5 秒 8.2 秒
Total Blocking Time 560 ミリ秒 420 ミリ秒 250 ミリ秒
Cumulative Layout Shift 0.057 0.057 0.057
Speed Index 6.4 秒 5.3 秒 6.5 秒

Note: Lighthouseのスコアはネットワークや端末状況に左右されるため、あくまで参考値としてご覧ください。

まとめ

すげーというだけでは記事にならないので、どう変わったかを確認するべく、app.component.ts での ngDoCheck の実行回数を確認してみました。

ngDoCheck実行回数
Zone-based 112回
Zoneless 4回

112回?!ってなりますよね。ちなみに app-component.tsion-router-outlet を含むコンポーネントなので、OnPush戦略ではありません。そこで、OnPush戦略に切り替えてみると、Zone-basedでも ngDoCheck は5回にまで減りました。逆に112回も何を再レンダリングしてるんだと思ったところ、ion-router-outlet 以下にあるコンポーネントって、動的にcreateComponentで生成されるんですね。で、createComponentされる度にView Treeが変化されるから、親でngAfterViewCheckedが実行されると。試しにIonicコンポーネントを追加したら、 ChangeDetectorRef の実行回数が増えたのでこれは確定ですね。

IonicコンポーネントもすべてAngularのComponentでラップされており、OnPushになっています(※ 生成された段階で ChangeDetectorRef からはdetachされます: https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/src/directives/proxies.ts

component-tree.png

これがZonelessになると、再レンダリングが抑制され、表示スピードが改善され、First Contentful Paintといった各種指標も改善されるようです。

今までのZone-basedな状態での速度や操作性も実用上問題が起きるようなことはありませんでしたが、それがより快適になるのはすばらしいですよね。Ionic Angularが現時点ではZonelessに公式では未対応ですが、検証を進めて、大きな問題がなければIonic teamにエスカレーションして公式対応まで進めることができればと思っています。

それではまた。

Discussion