Navigating the New Era of Angular: Zoneless Change Detection Unveiled

Netanel Basal
Netanel Basal
Published in
4 min readJan 10, 2024

--

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},
]);
}
  1. Change Detection Strategy: It sets ChangeDetectionScheduler to utilize ChangeDetectionSchedulerImpl, a class dedicated to managing change detection cycles.
  2. NgZone Optimization: It replaces the standard NgZone provider with NoopNgZone.

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);
}
}
no polyfills = no zone.js

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!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.