Angular Zoneless: Addio Zone.js

Angular Zoneless: Come cambia la Change Detection

Angular fin dalle prime release si è affidato alla libreria esterna Zone.js per rilevare i cambiamenti della UI ma finalmente Angular 18 è stato rilasciato e tra la novità più rilevanti troviamo l’introduzione del supporto sperimentale ad un nuovo tipo di Change Detection più efficiente e che semplifica il debugging dell’applicazione  rendendo più leggibili gli stacktrace.
Stiamo parlando di Angular Zoneless.

Cos’è Zone.js?

Zone.js è una libreria Javascript che, creando un contesto isolato, consente di tenere traccia delle operazioni asincrone come promises, callback, richieste XHR, gestione eventi e di eseguirle in modo controllato.

La principale funzione di questa importante libreria all’interno di Angular, è quella di inviare una notifica al framework ogniqualvolta viene conclusa un’operazione asincrona. 
Questo permette poi ad Angular di eseguire la change detection ed aggiornare la UI renderizzando nuovamente i compontenti del DOM che sono stati modificati.

In pratica la libreria aggiunge autonomamente un wrapper alle API asincrone di Javascript e inserisce il codice della callback in un contesto isolato chiamato “zona”. Quando il codice viene eseguito e la sua esecuzione giunge al termine, Angular viene notificato e inizia il ciclo di change detection.

Le criticità di Zone.js

Nonostante Angular e Zone.js siano veramente molto ben ottimizzati nello svolgere la rilevazione dei cambiamenti, in applicazioni complesse e ricche di operazioni asincrone questa tipologia di change detection può influire negativamente sulle prestazioni della webapp.
Questo perchè ogni volta che Zone.js rileva la conclusione di un blocco di codice asincrono, notifica ad Angular di eseguire l’aggiornamento della UI causando numerosi cicli di change detection e renderizzazione del DOM.

Benvenuta Zoneless Change Detection

La versione 18 rappresenta la prima tappa importante di un cammino iniziato alcuni anni fa dal team di Angular, verso l’abbandono di Zone.js.
Infatti con l’attuale release abbiamo accesso al supporto sperimentale ad Angular Zoneless ovvero la possibilità di abilitare la change detection basata sui componenti reattivi e senza l’ausilio di Zone.js

Per abilitarla sono necessarie alcune modifiche ai file “app.config.ts” e “angular.json
Vediamole nel dettaglio:

app.config.ts

import { ApplicationConfig, provideExperimentalZonelessChangeDetection, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from'@angular/router';
import { routes } from'./app.routes';
export const appConfig: ApplicationConfig = {
 providers: [
   // 👇 Rimuovi questa riga per disabilitare Zone.js
   provideZoneChangeDetection ({ eventCoalescing: true }),
   provideRouter(routes),
   // 👇 Aggiungi questa riga per abilitare Zoneless
   provideExperimentalZonelessChangeDetection()
 ],
};

angular.json

{
  [...]
  "projects": {
    "angular-v18-webapp": {
      [...]
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:application",
          "options": {
            [...]
            "polyfills": [
              // 👇 Rimuovi questa riga per rimuovere Zone.js dal bundle
              "zone.js"
            ]
          }
          [...]
        }
        [...]
      }
    }
}
}

Lo switch dalla change detection basata su Zone.js verso la nuova Angular Zoneless, ha già contribuito a ridurre le dimensioni del bundle della webapp di circa 10kb.
Per essere precisi, nelle mie prove le dimensioni del bundle si sono ridotte di 11,09kb:

angular zoneless dimensione bundle zone.js
Il Bundle con Zone.js ha dimensioni più importanti: 67.50kb
angular zoneless dimensione bundle zoneless
Il Bundle in modalità Zoneless ha dimensioni ridotte: 56.41kb

Angular Zoneless in pratica

Il miglior modo per sfruttare la change detection zoneless è quello di utilizzare i signals che sono stati introdotti in Angular v16 e resi stabili nella release successiva.
Questo perché rendendo i tuoi componenti reattivi, saranno i signals ad occuparsi di informare Angular che il modello è variato e che quindi è effettivamente necessario refreshare la UI renderizzandola nuovamente.
Inoltre se i tuoi componenti sono compatibili con la ChangeDetectionStrategy.OnPush non dovresti riscontrare particolari problemi nella transizione alla change detection Angular Zoneless, anche se ricorda che si tratta di un supporto sperimentale e quindi non del tutto stabile.

Qua sotto ho inserito un esempio pratico di componenti basati sui signals che si amalgano perfettamente con Angular Zoneless
Il componente “<app-user-list>” rappresenta una pagina con una lista di oggetti “User”, mentre il componente “<app-user-details>” rappresenta il dettaglio di uno specifico “User”.

Alla pressione del button “Dettagli“, l’oggetto “User” associato alla riga scelta dall’utente, viene scritto all’interno di un signal il cui valore viene poi passato in ingresso al componente figlio “<app-user-details>“.

Procediamo e diamo un’occhiata al codice:

user-list.component.ts

import { Component, WritableSignal, signal } from '@angular/core';
import { UserService } from '../../services/user.service';
import { AsyncPipe } from '@angular/common';
import { User } from '../../models/user';
import { UserDetailsComponent } from '../user-details/user-details.component';
import { Observable } from 'rxjs';
@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [AsyncPipe, UserDetailsComponent],
  templateUrl: './user-list.component.html',
  styleUrl: './user-list.component.scss',
})
export class UserListComponent {
protected users$: Observable = this.userSrv.getUsers();
// 👇 Inizializzo un WritableSignal che conterrà lo User
// selezionato dalla lista e lo passerà al componente "UserDetailsComponent"
protected currentUser: WritableSignal<User | undefined> = signal(undefined);
constructor(private userSrv: UserService) {}
protected showDetails(user: User) {
    if (user) {
      // 👇 Con il metodo "set", cambio il valore corrente del WritableSignal
      // Questa operazione informa Angular che qualcosa è cambiato e che quindi deve
      // renderizzare nuovamente il componente "UserDetailsComponent" che ottiene in ingresso
      // il valore del WritableSignal
      this.currentUser.set(user);
    }
}
}

user-list.component.html

<!--
  👇 In input al componente UserDetailsComponent, passo il WritableSignal.
  Per recuperare il valore del Signal è necessario invocarlo come una funzione
-->

<app-user-details [user]="currentUser()"></app-user-details>
<table>
  <thead>
    <th>Id</th>
    <th>Username</th>
    <th>Impiego</th>
    <th>Azioni</th>
  </thead>
  <tbody>
    @for (user of users$ | async; track $index) {
    <tr>
        <td>{{ user.id }}</td>
        <td>{{ user.username }}</td>
        <td>{{ user.jobType }}</td>
        <td><button (click)="showDetails(user)">Dettagli</button></td>
    </tr>
    }
  </tbody>
</table>

user-details.component.ts

import {
  ChangeDetectionStrategy,
  Component,
  InputSignal,
  input,
} from '@angular/core';
import { User } from '../../models/user';

@Component({
  selector: 'app-user-details',
  standalone: true,
  imports: [],
  templateUrl: './user-details.component.html',
  styleUrl: './user-details.component.scss',
// 👇 Imposto la strategia di Change Detection su "OnPush"
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserDetailsComponent{
  // 👇 Il Signal in ingresso dall'esterno del componente è di tipo "InputSignal"
  user: InputSignal<User | undefined> = input();
}

user-details.component.html

@if (user(); as user) {
  <p>Id Utente: {{ user.id }}</p>
  <p>Username: {{ user.username }}</p>
  <p>Nome: {{ user.name }}</p>
  <p>Cognome: {{ user.surname }}</p>
  <p>Impiego Attuale: {{ user.jobType }}</p>
}

Conclusioni

Angular Zoneless è senza dubbio un passo avanti significativo per quanto riguarda una migliore ottimizzazione dei cicli di Change Detection e delle performance generali delle webapplication basate su Angular.
Si tratta di una funzionalità ancora in via di perfezionamento ma comunque promettente e che soprattutto ci indica come sarà il prossimo futuro delle applicazioni Angular.

Se hai trovato questo articolo utile e interessante, ti invito a condividerlo e ad entrare in contatto con me tramite i miei recapiti e tramite LinkedIn.
Ti invito inoltre a dare un’occhiata ai miei servizi e in particolare a quello di sviluppo software.

Un caloroso saluto da Lorenzo Fadda – EFFEDeveloper 👋

Share via
Copy link