👌

Zone-based vs Zoneless: Performance Analysis in Ionic Angular

に公開

In my previous article, I introduced the concept of implementing Zoneless architecture in a small-scale web application.

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

However, for single-page web applications, the performance improvements and overhead reduction from going Zoneless are quite limited - to the point where the difference is barely noticeable. That's why I decided to experiment with implementing Zoneless in a medium-sized Ionic Angular application.

Performance Comparison: Zone-based vs Zoneless

Prerequisites

Ionic Angular's Zone.js Dependency

The @ionic/angular npm package includes zone.js as a peer dependency.

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

This means that even if you remove Zone.js from your project when going Zoneless, it will still be present in node_modules due to dependency requirements. Therefore, there's no bundle size reduction benefit from implementing Zoneless.

Interestingly, after investigation, I found that Ionic Angular only uses Zone.js internally in the runOutsideAngular method.

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

Ionic Angular's Current Zoneless Status

While Angular has been experimentally supporting Zoneless architecture (via provideExperimentalZonelessChangeDetection) since v16, Ionic Angular is currently designed with Zone.js as a core dependency and doesn't officially support Zoneless.

Moreover, the Ionic documentation (https://ionicframework.com/docs/angular/lifecycle) states the following about OnPush strategy:

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.

Not only is Zoneless not supported, but they even recommend against using OnPush strategy for components with ion-nav or ion-router-outlet! However, given the minimal Zone.js dependency, I decided to experiment with Zoneless implementation. Surprisingly, both ion-router-outlet and modals using ion-nav are working perfectly fine.

Note: We're currently testing this with a subset of users, and no issues have been reported so far.

While we're testing this with a production application that covers most use cases, it's important to note that Ionic hasn't officially announced Zoneless support. There's no guarantee that following this article will result in a working implementation. Additionally, future updates to Ionic or Angular might affect this behavior, so thorough testing is essential.

Hosting Environment

To ensure a fair comparison in a production-like environment, I've hosted both versions:

Both are hosted on Firebase Hosting, with the Zoneless version differing from Zone-based only in these aspects:

  • Components using ion-nav and ion-router-outlet are now using OnPush strategy
  • provideExperimentalZonelessChangeDetection API is imported
  • Zone.js is removed from the production build

All other aspects remain identical, ensuring a pure comparison. Both versions are connected to the same production backend and database.

Performance Results

Instead of continuous monitoring, I used Lighthouse (with throttling simulation) for local testing. After confirming consistent results across 10 initial runs, I performed 3 additional runs for this article, alternating between versions. Since the login page has fewer components and shows minimal differences, I focused on the item list page, which uses Virtual Scroll and contains multiple child components - making it ideal for comparing Zone-based and Zoneless performance.

The results were quite surprising - I expected the differences to be within the margin of error, but the contrast was significant.

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

zone-based.png

Run 1 Run 2 Run 3
First Contentful Paint 7.6s 8.6s 8.6s
Largest Contentful Paint 12.9s 12.8s 12.8s
Total Blocking Time 980ms 1,110ms 850ms
Cumulative Layout Shift 0.057 0.001 0.057
Speed Index 8.1s 8.6s 8.6s

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

zoneless.png

Run 1 Run 2 Run 3
First Contentful Paint 6.4s 4.9s 6.5s
Largest Contentful Paint 7.9s 7.5s 8.2s
Total Blocking Time 560ms 420ms 250ms
Cumulative Layout Shift 0.057 0.057 0.057
Speed Index 6.4s 5.3s 6.5s

Note: Lighthouse scores can vary based on network conditions and device performance, so please consider these results as reference values.

Conclusion

To understand these dramatic differences, I investigated the number of ngDoCheck executions in app.component.ts.

ngDoCheck Executions
Zone-based 112 times
Zoneless 4 times

112 times?! That's quite a number. Interestingly, app.component.ts contains ion-router-outlet and isn't using OnPush strategy. When I switched to OnPush strategy, the ngDoCheck executions in Zone-based mode reduced to just 5 times. This led me to investigate why there were so many re-renders, and I discovered that components under ion-router-outlet are dynamically created using createComponent. Each time a component is created, the View Tree changes, triggering ngAfterViewChecked in the parent component. Adding more Ionic components increased the ChangeDetectorRef execution count, confirming this behavior.

Ionic components are all wrapped in Angular Components and use OnPush strategy (though they're detached from ChangeDetectorRef upon creation: https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/src/directives/proxies.ts)

component-tree.png

With Zoneless implementation, re-rendering is significantly reduced, leading to improved display speed and better performance metrics like First Contentful Paint.

While the previous Zone-based implementation was perfectly usable without any practical issues, the enhanced performance and responsiveness with Zoneless is quite remarkable. Although Ionic Angular doesn't officially support Zoneless at this time, I'm continuing to test this implementation. If no major issues arise, I plan to escalate this to the Ionic team to potentially achieve official support.

Until next time!

Discussion