This is the second part of a two-part blog series: Going Zoneless! If you’re unfamiliar with the magic behind Zone.js, I recommend checking out the first post!
Advantages of Zoneless
Going Zoneless brings several advantages, including increased predictability. The magic provided by Zone.js
is gone, and you now have more fine-grained control over the re-renders.
- Performance improvements
- Eliminates overhead from
Zone.js
patching async APIs. - Reduced bundle size.
- Change detection only runs when necessary.
- Eliminates overhead from
- Improved debugging experience
- Requires manual view updates, making asynchronous behavior more explicit and predictable.
- Better ecosystem compatibility
- Easier integration with non-Angular libraries and third-party tools.
Note: depending on your app’s complexity and async usage, the performance improvements may be minimal.
Going Zoneless
To start the process of going Zoneless, your application must complete the following steps:
- Uninstall the zone.js dependency and remove its reference from the
polyfills
inangular.json
.- Remove references to
NgZone.onMicrotaskEmpty
,NgZone.onUnstable
,NgZone.isStable
, &NgZone.onStable
. You can explore usingafterNextRender
,afterRender
, or the nativeMutationObserver
when code needs to wait for a certain DOM state. NgZone.run
&NgZone.runOutsideAngular
still work! This may be required for some libraries that still use Zone.js.
- Remove references to
- Enable Zoneless by changing how you bootstrap the app:
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
// standalone bootstrap
bootstrapApplication(MyApp, {
providers: [
provideExperimentalZonelessChangeDetection(),
provideRouter(routes)
]
});
Your Angular application should now be zoneless! 🎉
Next steps
Migrate any components that use async APIs to be compatible with zoneless change detection. I strongly urge you to understand how to use signals! Updating a signal will trigger a change detection for the component.
I’ve created several before-and-after examples to demonstrate how to migrate your logic. The examples mainly highlight how going zoneless impacts ChangeDetectionStrategy.Default
strategy as the migration should be fairly easy for OnPush
components.
Using OnPush
strategy
If you’re using OnPush
strategy for your components, your application will likely work out of the box in zoneless!
Yup, you read that right! Angular now offers a more convenient way to trigger change detection using signals. When a signal updates, Angular automatically schedules change detection for the component and its subtree. No need to manually call markForCheck anymore!
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component
} from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>{{ message }}</p>
<button (click)="updateMessage()">Update Message</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
message = 'Initial message';
constructor(private cdr: ChangeDetectorRef) {}
updateMessage() {
setTimeout(() => {
this.message = 'Message updated!';
this.cdr.markForCheck();
}, 3000);
}
}
import {
ChangeDetectionStrategy,
Component,
signal
} from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>{{ message() }}</p>
<button (click)="updateMessage()">Update Message</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
message = signal('Initial message');
updateMessage() {
setTimeout(() => {
// **Note**: You can still use the `before` approach. This is just a quality of life improvement to use signals
this.message.set('Message updated!');
}, 3000);
}
}
HTTP Request Example
This example showcases how Zoneless is similar to using the OnPush
strategy in your current app.
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
template: `
<p>
@if (data) {
{{ data.name }}
} @else {
Loading...
}
</p>
<button (click)="fetchData()">Fetch Data</button>
`,
})
export class AppComponent {
data: any;
constructor(private http: HttpClient) {}
fetchData() {
this.http.get<any>('/api/users/1').subscribe(response => {
this.data = response;
});
}
}
import { Component, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
template: `
<p>
@if (data()) {
{{ data().name }}
} @else {
Loading...
}
</p>
<button (click)="fetchData()">Fetch Data</button>
`,
})
export class AppComponent {
data = signal<any>(null);
constructor(private http: HttpClient) { }
fetchData() {
this.http.get<any>('/api/users/1')
.subscribe(response => {
// You can also manually call `markForCheck` from `ChangeDetectorRef`
// if you do NOT want to use signals
this.data.set(response);
});
}
}
Notice that in the “after” example, a signal is being used. You could still use a regular variable and markForCheck
.
Using Default
strategy
If the component is NOT using any async APIs, no change should be required as outlined in the below example. Angular will still track variables used in the template and update the view when they change.
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `<button (click)="increment()">Increment</button> {{ count }}`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
}
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `<button (click)="increment()">Increment</button> {{ count }}`
})
export class CounterComponent {
count = 0;
increment() {
this.count++;
}
}
Closing thoughts
I believe this is a step in the right direction. Making Angular lighter, faster, and less “magical” is a win for both performance and developer experience.
In fact, I’m currently exploring how I can make carbon-components-angular support zoneless. What’s your experience been going zoneless? Do share your experience in the comments below.
See you next time! 🚀