1 of 60

2 of 60

  1. What is Change Detection? How does it work with Zone.js?

  • What does removing Zone.js mean for Angular?�How does Angular Change Detection work when going Zoneless?

  • How can you "Get ready to go Zoneless"?�What parts of the codebase need refactoring?

3 of 60

What is Change Detection? How does it work with Zone.js?

4 of 60

let count = 1;

count = 5;

let count = 1;

⚡️

⚡️

⚡️

⚡️

⚡️

5 of 60

@Component({

selector: "my-component",

template: `

<div>{{ counter }}</div>

<button (click)="increase()"></button>

`,

})

export class MyComponent {

count = 0;

increase(): void {

this.count++;

}

}

@Component({

selector: "my-component",

template: `

<div>{{ counter }}</div>

<button (click)="increase()"></button>

`,

})

export class MyComponent {

count = 0;

increase(): void {

setInterval(() => {

this.count++;

}, 1000);

}

}

Zone.js

Z

Powered by

6 of 60

Monkey patching is a technique to modify or extend existing code

at runtime without altering the original source code

This triggers Angular change detection

Zone.js

Z

const originalSetTimeout = window.setTimeout;

window.setTimeout = function (fn, ms) {

originalSetTimeout(fn, ms);

this.applicationRef._tick();

};

originalSetTimeout(..., ...);

7 of 60

MacroTask

MicroTask

EventTask

  • setTimeout( )
  • clearTimeout( )
  • setInterval( )
  • ...
  • Promise
  • MutationObserver
  • ...
  • DOM event
  • Event listeners
  • ...

Zone.js

Z

8 of 60

AppComponent

ApplicationRef

9 of 60

NgZone

AppComponent

ApplicationRef

ApplicationRef

AppComponent

Event

10 of 60

<img

[alt]="hero.name"

[src]="hero.imageUrl"

/>

<app-hero-detail

[hero]="hero"

/>

<main>

<h2>Hello {{ firstName }}!</h2>

<input type="text" [(ngModel)]="firstName" />

</main>

@if(show){

@for (item of list; track item.id) {

<p>{{ item.name }}</p>

<img [src]="item.image" />

}

}

11 of 60

Angular default change detection

12 of 60

  1. Input values update
  2. Angular triggers an event using event binding, output binding or @HostListener
  3. Marked as dirty using the method changeDetectorRef.markForCheck( )

13 of 60

NgZone

OnPush

AppComponent

ApplicationRef

ApplicationRef

AppComponent

Event

OnPush

14 of 60

NgZone

OnPush

AppComponent

ApplicationRef

ApplicationRef

Event

OnPush

OnPush

AppComponent

AppComponent

15 of 60

Let's migrate every component to

OnPush startegyyy

16 of 60

@Component({

selector: 'my-component',

template: `

<button (click)="addItem()">Add</button>

<my-child [items]="items" />

`

})

export class MyComponent {

items = [];

addItem() {

this.items.push(42);

this.items = [...this.items, 42];

}

}

@Component({

selector: 'my-component',

template: `

<button (click)="loadData()">Load</button>

<my-child [items]="items" />

`

})

export class MyComponent {

cdr = inject(ChangeDetectorRef);

myService = inject(MyService);

items = [];

loadData() {

this.myService.getData().subscribe(data => {

this.items = data;

this.cdr.markForCheck();

})

}

}

17 of 60

Spread operators and markForCheck( )

EVERYWHERE

18 of 60

@Component({

selector: 'app-map',

template: `

<div id="map" style="width: 100%; height: 500px;"></div>

<p>Cords: {{ cords.lat }}, {{ cords.lng }}</p>

`

})

export class MapComponent implements OnInit {

private ngZone: NgZone = inject(NgZone);

map: mapLib.Map;

cords: { lat: number, lng: number } = {};

ngOnInit() {

this.ngZone.runOutsideAngular(() => {

this.map = new mapLib.Map({ container: 'map', center: [12.4964, 41.9028] });

this.map.on('move', (lat, lng) => {

this.ngZone.run(() => {

this.cords = { lat, lng };

});

});

});

}

}

19 of 60

Default

OnPush

Checks all components binded values on every

event or model change

Skip checks unless

  • input values change
  • internal events occur
  • markForCheck( )

Performance

DevEx

20 of 60

21 of 60

const name = signal(Mario);

const lastName = signal(Rossi);

firstName.set('Giorgio');

firstName.set('Matteo');

const fullName = computed(() => `${name()} ${lastName()}`);

effect(() => {

console.log(`Full Name: ${fullName()}`);

});

22 of 60

23 of 60

const mySignal = signal(0);

effect(() => console.log('Value:', mySignal())); // Value: 3

mySignal.set(1);

mySignal.set(2);

mySignal.set(3);

24 of 60

25 of 60

Angular templates can now track

Signals updates using effect( )

<div>

{{ description() }}

</div>

<app-hero-detail

[hero]="hero()"

/>

@if(show()){

@for (item of list(); track item) {

<p>{{ item.name }}</p>

<img [src]="item.image" />

}

}

26 of 60

NgZone

OnPush

Signal update

AppComponent

ApplicationRef

ApplicationRef

OnPush

OnPush

OnPush

AppComponent

AppComponent

OnPush

Global Mode

Targeted Mode

27 of 60

This is great...

BUT

Zone.js

Z

28 of 60

29 of 60

30 of 60

Zone on, Zone off

31 of 60

Circa 15 minuti

fino a qui

32 of 60

33 of 60

NgZone

ApplicationRef

AppComponent

34 of 60

AppComponent

ApplicationRef

ChangeDetectionScheduler

35 of 60

requestAnimationFrame

setTimeout

ChangeDetectionScheduler

36 of 60

Without Zone.js, Angular can only rely on its own APIs

These APIs include:

  • ChangeDetectorRef.markForCheck( )
  • Updating a Signal that is read in a template
  • Host or template listeners are triggered
  • ComponentRef.setInput( )
  • Attaching and detaching a view
  • Registering a render hook
  • AsyncAnimationLoad
  • Deferrable views updates

ChangeDetectionScheduler

37 of 60

Without Zone.js, Angular can only rely on its own APIs

These APIs include:

  • ChangeDetectorRef.markForCheck( )
  • Updating a Signal that is read in a template
  • Host or template listeners are triggered
  • ComponentRef.setInput( )
  • Attaching and detaching a view
  • Registering a render hook
  • AsyncAnimationLoad
  • Deferrable views updates

This triggers Angular change detection starting from v18

ChangeDetectionScheduler

38 of 60

New experimental Zoneless change detection provider

export const appConfig: ApplicationConfig = {

providers: [

provideExperimentalZonelessChangeDetection(),

...

],

};

39 of 60

{

"name": "my-app",

"dependencies": {

"@angular/compiler": "^18.0.0",

"@angular/core": "^18.0.0",

...,

"rxjs": "~7.8.0",

"tslib": "^2.3.0",

"zone.js": "~0.14.3"

}

}

{

"projects": {

"my-app": {

"architect": {

"build": {

"options": {

"polyfills": [

"zone.js"

],

},

...

{

"projects": {

"my-app": {

"architect": {

"build": {

"options": {

"polyfills": [

"zone.js"

"zone.js/testing"

],

},

...

40 of 60

Bye Bye Zone.js

41 of 60

App not working

42 of 60

TIME TO REFACTOR

43 of 60

Demo structure:

  • Migration to Zoneless
  • Refactor the app where necessary
  • Reintegrate Zone as a demonstration of the compatibility of our changes

Pescara

44 of 60

DEMO TIME

45 of 60

Steps to Get ready to go Zoneless:

  • Migrate each component to OnPush change detection
    • Using markForCheck( ) and AsyncPipe
    • Using Signals on your templates

  • Replace NgZone onStable( ) and onMicrotaskEmpty( ) methods�with afterRender( ) and afterNextRender( )
  • Zoneless only: remove NgZone run*( ) methods and just call your function

46 of 60

Steps to Get ready to go Zoneless:

  • Migrate each component to OnPush change detection
    • Using markForCheck( ) and AsyncPipe
    • Using Signals on your templates

  • Replace NgZone onStable( ) and onMicrotaskEmpty( ) methods�with afterRender( ) and afterNextRender( )
  • Zoneless only: remove NgZone run*( ) methods and just call your function

47 of 60

Steps to Get ready to go Zoneless:

  • Migrate each component to OnPush change detection
    • Using markForCheck( ) and AsyncPipe
    • Using Signals on your templates

  • Replace NgZone onStable( ) and onMicrotaskEmpty( ) methods�with afterRender( ) and afterNextRender( )
  • Zoneless only: remove NgZone run*( ) methods and just call your function

48 of 60

export const appConfig: ApplicationConfig = {

providers: [

provideExperimentalZonelessChangeDetection(),

provideExperimentalCheckNoChangesForDebug({

interval: 2000,

// useNgZoneOnStable: true,

exhaustive: true

}),

...

],

};

49 of 60

Embrace the Signals APIs

50 of 60

@Component({ ... })

export class MyComponent {

myInputEl = viewChildren('inputEl'); // Signal<readonly ElementRef[]>

myContentEl = contentChild('contentEl'); // Signal<ElementRef | undefined>

myProp = input<string>(); // InputSignal<string, string>

myRequiredProp = input.required<string>(); // InputSignal<string, string>

myModel = model<string>(); // ModelSignal<string>

myRequiredModel = model.required<string>(); // ModelSignal<string>

}

51 of 60

import { resource } from "@angular/core";

@Component({})

export class MyComponent {

todoResource = resource({

loader: () => {

return Promise.resolve({ id: 1, title: "Hello World", completed: false });

},

});

constructor() {

effect(() => {

console.log("Value: ", this.todoResource.value());

console.log("Status: ", this.todoResource.status());

console.log("Error: ", this.todoResource.error());

})

}

}

52 of 60

53 of 60

OnPush

Skip checks unless

  • input values change
  • internal events occur
  • markForCheck( )

Performance

DevEx

Default

Checks all components binded values on every

event or model change

54 of 60

Bye bye Zone.js� �Easy to dev and highly optimized with Signals

Zoneless

DevEx

Performance

55 of 60

56 of 60

Davide Passafaro

  • Senior Frontend Engineer @
  • Angular, NgRx, Capacitor, Electron
  • Angular Rome Community Lead
  • GDG Roma Città Organizer (NEW)

davide-passafaro

DavidePassafaro

@DaveloperIT

57 of 60

58 of 60

59 of 60

60 of 60

QUESTION TIME