I introduced zoneless migration for a small website in the article below.
https://zenn.dev/rdlabo/articles/eab1f30b780ce0
On a single-page marketing site, though, zoneless gains and overhead reduction are limited—I could not feel a difference. So I tried zoneless on a mid-size Ionic Angular app.
Zone-based vs zoneless performance
Prerequisites
Ionic Angular is a Zone.js-dependent library
The npm package @ionic/angular lists zone.js in peerDependencies.
https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/package.json#L60C6-L60C13
So even if you go zoneless and remove Zone.js from the project, Zone.js still remains in node_modules through dependencies. Zoneless therefore has no effect on bundle size reduction.
However, as far as I checked internally, Zone.js is used only through the runOutsideAngular method.
https://github.com/search?q=repo%3Aionic-team%2Fionic-framework+this.z.&type=code
Ionic Angular does not support zoneless yet
Angular has supported zoneless experimentally since v16 (provideExperimentalZonelessChangeDetection), but Ionic Angular is designed around Zone.js, so there is no official support yet.
Also, https://ionicframework.com/docs/angular/lifecycle says this about OnPush:
Components that use
ion-navorion-router-outletshould not use theOnPushchange detection strategy. Doing so will prevent lifecycle hooks such asngOnInitfrom firing. Additionally, asynchronous state changes may not render properly.
So not only zoneless—the docs even warn against OnPush for some components. And yet, because Ionic's Zone.js dependency is thin, I tried zoneless anyway and confirmed that both ion-router-outlet and modals using ion-nav work normally.
Note: I asked some users to test an experimental build, and so far I have found no bugs.
I based this on an app many users already run, so I believe common patterns are covered, but Ionic has not announced zoneless support. Following this article does not guarantee zoneless will work for you. Future Ionic or Angular upgrades may change behavior, so test thoroughly.
Hosted in the same environment
For a production Angular build and conditions close to real use, I hosted comparison builds as follows:
- Zone-based: https://zone-based.winecode.app/
- Zoneless: https://zoneless.winecode.app/
Both are on Firebase Hosting. Zoneless differs from zone-based only as follows:
- Components using
ion-navandion-router-outletalso use OnPush - Import
provideExperimentalZonelessChangeDetection - Remove Zone.js from the product bundle
There are no other differences, so the comparison is clean. Both backends (and databases) connect to production.
Measurement results
This is not continuous monitoring—I compared locally with Lighthouse (throttled simulation). I ran it 10 times and confirmed the same trend each time, then alternated three more runs each for this article. The tables below are from those last three runs. The login page has few components and almost no difference, so I measured the item list page shown after login. It uses Virtual Scroll with multiple child components, where I expected zone-based vs zoneless to diverge.
Honestly, I expected noise-level differences, so I was surprised how clear the gap was.
Zone-based: https://zone-based.winecode.app/

| Run 1 | Run 2 | Run 3 | |
|---|---|---|---|
| First Contentful Paint | 7.6 sec | 8.6 sec | 8.6 sec |
| Largest Contentful Paint | 12.9 sec | 12.8 sec | 12.8 sec |
| Total Blocking Time | 980 ms | 1,110 ms | 850 ms |
| Cumulative Layout Shift | 0.057 | 0.001 | 0.057 |
| Speed Index | 8.1 sec | 8.6 sec | 8.6 sec |
Zoneless: https://zoneless.winecode.app/

| Run 1 | Run 2 | Run 3 | |
|---|---|---|---|
| First Contentful Paint | 6.4 sec | 4.9 sec | 6.5 sec |
| Largest Contentful Paint | 7.9 sec | 7.5 sec | 8.2 sec |
| Total Blocking Time | 560 ms | 420 ms | 250 ms |
| Cumulative Layout Shift | 0.057 | 0.057 | 0.057 |
| Speed Index | 6.4 sec | 5.3 sec | 6.5 sec |
Note: Lighthouse scores depend on network and device conditions, so treat them as reference values only.
Summary
"Wow" alone does not make an article, so I checked what changed by counting ngDoCheck runs in app.component.ts.
| ngDoCheck runs | |
|---|---|
| Zone-based | 112 |
| Zoneless | 4 |
112?! Right. By the way, app-component.ts includes ion-router-outlet, so it is not OnPush. Switching it to OnPush reduced ngDoCheck to 5 even in zone-based mode. That made me wonder what was re-rendering 112 times. Components under ion-router-outlet are created dynamically with createComponent. Each creation changes the view tree, so when the parent runs ngAfterViewChecked, it fires. Adding Ionic components increased ChangeDetectorRef runs, so this explanation holds.
Every Ionic component is wrapped as an Angular component and uses OnPush (※ they detach from ChangeDetectorRef when generated: https://github.com/ionic-team/ionic-framework/blob/main/packages/angular/src/directives/proxies.ts )

Zoneless suppresses those re-renders, which improves display speed and metrics such as First Contentful Paint.
Zone-based speed and usability were already fine in production, but making it more comfortable is great. Ionic Angular is not officially zoneless yet, but if validation continues without major issues, I hope to escalate to the Ionic team and push toward official support.
See you next time.