Navigating the New Era of Angular: Zoneless Change Detection Unveiled
The Angular framework has always been at the forefront of advancing web application development. In its latest stride towards efficiency and performance optimization, the Angular team has introduced in v18.0.0-next.5 an intriguing feature: provideExperimentalZonelessChangeDetection
. This new functionality heralds a shift towards a 'zoneless' future. Let's delve into the technicalities of this feature and understand its implications.
Initial Setup
To embark on using zoneless change detection, we first import the provideExperimentalZonelessChangeDetection
function into our application providers:
bootstrapApplication(AppComponent, {
providers: [
// 👇 Say goodbye to Zone
provideExperimentalZonelessChangeDetection(),
],
});
Next, we ensure that Zone.js is removed from the polyfills
section of your angular.json
file:
{
"projects": {
"app": {
"architect": {
"build": {
"options": {
"polyfills": [
"zone.js"
],
}
}
}
}
}
}
The Core Functionality
The provideExperimentalZonelessChangeDetection
function is a significant addition to Angular's toolkit. It essentially configures two primary providers in the Angular environment:
export function provideExperimentalZonelessChangeDetection() {
return makeEnvironmentProviders([
{
provide: ChangeDetectionScheduler,
useExisting: ChangeDetectionSchedulerImpl
},
{provide: NgZone, useClass: NoopNgZone},
{provide: ZONELESS_ENABLED, useValue: true},
]);
}
- Change Detection Strategy: It sets
ChangeDetectionScheduler
to utilizeChangeDetectionSchedulerImpl
, a class dedicated to managing change detection cycles. - NgZone Optimization: It replaces the standard
NgZone
provider withNoopNgZone
.
The ChangeDetectionSchedulerImpl
class is elegantly simple yet powerful. This method introduces a notify
function, which internally invokes scheduleCallback
. This function is responsible for scheduling the execution of ApplicationRef.tick()
either on a timer
or using requestAnimationFrame
. This method is crucial as it triggers top-to-bottom change detection across the application.
Here’s a closer look at its implementation:
@Injectable({ providedIn: 'root' })
class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
notify(): void {
this.cancelScheduledCallback = scheduleCallback(() => {
this.tick(this.shouldRefreshViews);
}, false);
}
}
The notify
function plays a pivotal role within Angular's internal markViewDirty
and markAncestorsForTraversal
functions, which itself is triggered in various scenarios like markForCheck
, template events, setInput
, signal updates, and more.
The concept behind the updated Zoneless change detection is centered on the principle of components promptly informing Angular about any changes. This eliminates the need for an intermediary layer like Zone.js to signal Angular about changes.
Traditionally, Angular employs various APIs to indicate changes. These include:
- Invoking
ChangeDetectorRef.markForCheck()
. - Utilizing
ComponentRef.setInput
- Updating the value of a signal referenced in the template
- Triggering template event listeners
- Attaching or detaching a view using
ViewContainerRef
To illustrate the practical application of this new functionality, consider the following dummy component example:
@Component({
selector: 'app-foo',
standalone: true,
imports: [AsyncPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>markForCheck: {{ markForCheck }}</p>
<p>signal: {{ sig() }}</p>
<p>byEvent: {{ byEvent }}</p>
<p>Async pipe: {{ num$ | async }}</p>
<button (click)="byEvent = 3">Click</button>`,
})
export class FooComponent {
markForCheck = 1;
sig = signal(1);
byEvent = 2;
num$ = interval(1500);
private cdr = inject(ChangeDetectorRef);
ngOnInit() {
// In real life you'll also call cleatInterval
setInterval(() => {
this.markForCheck += 1;
this.cdr.markForCheck();
}, 1000);
setTimeout(() => {
this.sig.set(2);
}, 500);
}
}
This example highlights how the new functionality can be seamlessly integrated into standard Angular workflows, offering a zoneless alternative without sacrificing application responsiveness or functionality.
This advancement is quite remarkable. It implies that reliance solely on signals for achieving zoneless applications is no longer a necessity. Provided that your application employs the async
pipe or utilizes the markForCheck
method in conjunction with observables, it should continue to operate as expected.
However, it’s important to note that in the absence of zone.js
's automatic change detection, one must explicitly trigger change detection using one of the prescribed methods; failing to do so will result in change detection not being executed.
Expanding Change Detection Beyond the Zone
In this latest Angular update, significant improvements have been made to the handling of change detection. Now, Angular ensures the consistency of change detection even when state updates originate from outside the zone. For instance, consider a scenario where a method explicitly informs Angular of an update, such as updating a signal:
class FooComponent {
zone = inject(NgZone);
counter = signal(0);
ngOnInit() {
this.zone.runOutsideAngular(() => {
setInterval(() => {
this.counter.update(c => c + 1)
}, 5000);
})
}
}
Even though the code runs outside the zone, a change detection cycle is scheduled due to the signal update. Notably, it’s the signal update responsible for triggering the change detection cycle, not the setInterval
function.
Impact on Unit Tests
This refinement in change detection behavior may result in differences observed in existing unit tests. While updating tests for accuracy is generally recommended, the tems understand that debugging efforts may outweigh the benefits in some cases. As a solution, tests can revert to the previous behavior by including the following in the TestBed providers:
provideZoneChangeDetection({
schedulingMode: NgZoneSchedulingMode.NgZoneOnly
})
Customizing Change Detection Behavior
For applications requiring updates to state outside the zone without triggering change detection, customization options are available, using the schedulingMode option:
bootstrapApplication(App, {
providers: [
provideZoneChangeDetection({
schedulingMode: NgZoneSchedulingMode.NgZoneOnly
}),
],
});
Alternatively, add schedulingMode: NgZoneSchedulingMode.NgZoneOnly
to the BootstrapOptions
of bootstrapModule
.
Important Considerations
It’s crucial to remember that it’s still in its nascent stages and subject to verification and potential modifications.
Follow me on Medium or Twitter to read more about Angular and JS!