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.
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.
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.
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
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.
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:
- Zone-based: https://zone-based.winecode.app/
- Zoneless: https://zoneless.winecode.app/
Both are hosted on Firebase Hosting, with the Zoneless version differing from Zone-based only in these aspects:
- Components using
ion-nav
andion-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.
https://zone-based.winecode.app/
Zone-based: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 |
https://zoneless.winecode.app/
Zoneless: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)
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