Ionic AngularアプリのZone-basedとZonelessのパフォーマンス比較と考察
以下の記事で規模の小さいWebサイトのZoneless化をご紹介しました。
しかしながら、ペライチでのWebサイトでのZoneless化では、パフォーマンス改善やオーバーヘッド削減効果は限定的です。というか、差を感じることはできません。そこで、中規模程度のIonic AngularアプリをZoneless化してみました。
Zone-basedとZonelessのパフォーマンス比較
前提条件
Ionic AngularはZone.js依存のライブラリ
npmパッケージ @ionic/angular
は、peerDependenciesに zone.js
があります。
そのため、Zonelessにして、プロジェクトからZone.jsを削除しても、Zone.js自体は依存関係上、node_modulesにはいます。ですので、Bundleサイズ削減効果に関してはZoneless化は一切の影響はありません。
ただ、内部でZone.jsに依存しているかといえば、調べた限りでは runOutsideAngular
メソッドで利用してるだけでした。
Ionic Angularは、Zoneless未対応
Angular自体はv16以降でZoneless(provideExperimentalZonelessChangeDetection
)を実験的にサポートしていますが、Ionic AngularはZone.js前提で設計されているため、公式サポートは現時点でありません。
また、 https://ionicframework.com/docs/angular/lifecycle で、OnPush戦略についてこう書かれています。
Components that use
ion-nav
orion-router-outlet
should not use theOnPush
change detection strategy. Doing so will prevent lifecycle hooks such asngOnInit
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-outlet
も ion-nav
を使ったモーダルも正常に動作しているのを確認しています。
Note: 実験中バージョンとして一部のユーザにテスト運用を依頼していますが、現在のところ不具合は見つかっていません
実際に多くのユーザに使ってもらっているアプリをベースにZoneless化しているので、一通りのパターンも実装されているとは思いますが、公式ではZoneless対応は発表しておらず、この記事を参考にZoneless化しても正常に動く保証はありません。また、今後のIonicやAngularのバージョンアップで動作が変わる可能性もありますので、十分なテストが必要です。
同一環境でホスティング
Angularのプロダクションビルドで、かつ実運用に近づけるため、以下で比較用にホスティングしました。
- Zone-based:https://zone-based.winecode.app/
- Zoneless: https://zoneless.winecode.app/
Firebase Hostingで、Zonelessは、Zone-basedから以下のように変更したものです。
-
ion-nav
とion-router-outlet
を使ってるコンポーネントもOnPush戦略に変更 -
provideExperimentalZonelessChangeDetection
APIをインポート - Zone.jsをプロダクトから削除
これ以外の差はありませんので、純粋に比較が可能です。なお、両方バックエンド(とデータベース)は本番用につながっています。
計測結果
定点観測ではなく、ローカルでLighthouse(ストットリングのシュミレーション)を使って比較しています。10回実行して10回とも同じ傾向がでていることを確認したあと、交互に3回ずつ追加で記事執筆用に実行しており、計測結果はその最後の3回です。ログインページはコンポーネント数も少なく、差はほとんどでませんので、ログインして表示される、アイテム一覧ページを対象にしました。Virtual Scrollを使っており、子コンポーネントが複数あるため、Zone-basedとZonelessで差が期待できたためです。
以下の通りです。正直なところ、誤差レベルだと思ってたので、あまりに差が如実で驚きました。
https://zone-based.winecode.app/
Zone-based: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 秒 |
https://zoneless.winecode.app/
Zoneless: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.ts
は ion-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 )
これがZonelessになると、再レンダリングが抑制され、表示スピードが改善され、First Contentful Paintといった各種指標も改善されるようです。
今までのZone-basedな状態での速度や操作性も実用上問題が起きるようなことはありませんでしたが、それがより快適になるのはすばらしいですよね。Ionic Angularが現時点ではZonelessに公式では未対応ですが、検証を進めて、大きな問題がなければIonic teamにエスカレーションして公式対応まで進めることができればと思っています。
それではまた。
Discussion