Angular 19 Zoneless: Performance e Change Detection senza Zone.js

Guida tecnica approfondita alla Zoneless Change Detection in Angular 19 e 20. Funzionamento della reattività basata su Signals, attivazione di provideZonelessChangeDetection, insidie nella migrazione con setTimeout e Reactive Forms, SSR senza Zone.js e benchmark prestazionali.

Diagramma della Zoneless Change Detection in Angular 19 con Signals al posto di Zone.js

Angular 19 segna un punto di svolta nell'architettura del framework: l'introduzione sperimentale della Zoneless Change Detection permette di rimuovere completamente Zone.js dal bundle applicativo, delegando il rilevamento delle modifiche di stato al sistema nativo di Signals. Questa transizione architetturale non si limita a una semplice riduzione delle dimensioni del bundle di oltre 10 KB compressi, ma elimina alla radice un'intera categoria di problemi prestazionali causati dal patching sistematico di oltre 130 API asincrone del browser operato da Zone.js.

Per chi sviluppa con Angular nel 2026, padroneggiare il funzionamento della Zoneless Change Detection costituisce una competenza imprescindibile, tanto nella pratica quotidiana quanto nella preparazione a domande colloquio Angular sulla change detection. L'argomento attraversa trasversalmente architettura, performance e Signals, tre temi ricorrenti nelle interviste tecniche su Angular.

Cronologia dell'adozione Zoneless

Angular 18 ha introdotto il primo supporto sperimentale alla Zoneless Change Detection come Developer Preview. Angular 19 ha consolidato l'API con provideExperimentalZonelessChangeDetection() e il pieno supporto ai Signals. Angular 20 ha promosso Zoneless a configurazione raccomandata per i nuovi progetti con provideZonelessChangeDetection() stabile. Angular 21, previsto per l'autunno 2026, genererà progetti senza Zone.js per impostazione predefinita. La migrazione delle applicazioni esistenti può avvenire in modo graduale, poiché Zone.js e la modalità Zoneless possono coesistere nello stesso progetto.

Il meccanismo della Change Detection con Zone.js

Per comprendere appieno i vantaggi dell'architettura Zoneless occorre innanzitutto analizzare il meccanismo che essa sostituisce. Zone.js è una libreria che sovrascrive tramite monkey patching tutte le API asincrone del browser: setTimeout, setInterval, Promise.then, addEventListener, XMLHttpRequest, fetch e decine di altre interfacce. Angular crea un'istanza dedicata chiamata NgZone che monitora ciascuna di queste operazioni intercettate.

Quando un'operazione asincrona si completa — ad esempio una risposta HTTP o la scadenza di un timer — Zone.js notifica NgZone. Quest'ultima avvia un ciclo completo di change detection che attraversa l'intero albero dei componenti dalla radice verso le foglie, confrontando i valori precedenti con quelli correnti per ogni binding nel template.

app.config.ts - Traditional Zone.js setup (Angular 18-19)typescript
import { ApplicationConfig } from '@angular/core';
import { provideZoneChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    // Zone.js patches ~130+ browser APIs
    // Every async callback triggers change detection
  ]
};

Il problema fondamentale di questo approccio risiede nella granularità: Zone.js non sa quali dati siano cambiati, sa soltanto che qualche operazione asincrona si è conclusa. Di conseguenza Angular deve verificare l'intero albero dei componenti ad ogni ciclo. In applicazioni di grandi dimensioni con centinaia di componenti, questo comportamento genera un overhead computazionale significativo e misurabile. Ogni richiesta HTTP, ogni timer, ogni listener di eventi produce un attraversamento completo dell'algoritmo di change detection, anche quando il dato modificato riguarda un singolo componente foglia.

Attivare la Zoneless Change Detection in Angular 19 e 20

L'attivazione della Zoneless Change Detection avviene tramite una provider function nella configurazione dell'applicazione. In Angular 19 l'API è contrassegnata come sperimentale, mentre a partire da Angular 20 diventa stabile.

app.config.ts - Angular 19 (experimental)typescript
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    // No more Zone.js patching
  ]
};
app.config.ts - Angular 20+ (stable)typescript
import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZonelessChangeDetection(),
  ]
};

Dopo aver configurato il provider, occorre rimuovere Zone.js dalla configurazione di build e disinstallare il pacchetto:

bash
# Remove zone.js polyfill from angular.json build and test targets
# Then uninstall the package
npm uninstall zone.js

A questo punto l'applicazione si avvia senza Zone.js. Angular perde la capacità di rilevare automaticamente il completamento delle operazioni asincrone e si affida interamente al sistema di Signals e a notifiche esplicite per orchestrare la change detection.

Trigger della Change Detection in modalità Zoneless

Senza Zone.js, Angular necessita di meccanismi alternativi per determinare quando lo stato di un componente è cambiato. La fonte primaria di trigger per la change detection sono i Signals, la primitiva reattiva disponibile da Angular 16 e divenuta il metodo preferito di gestione dello stato in Angular 19.

Quando il valore di un Signal cambia, Angular contrassegna come "dirty" esclusivamente i componenti che leggono quel Signal nel proprio template. Solo quei componenti e i relativi antenati nell'albero vengono verificati nel ciclo successivo. Questa granularità costituisce il vantaggio prestazionale decisivo rispetto all'approccio con Zone.js.

counter.component.ts - Signal-driven change detectiontypescript
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: \`
    <div class="counter">
      <button (click)="decrement()">-</button>
      <span>{{ count() }}</span>
      <button (click)="increment()">+</button>
      <p>Double: {{ doubled() }}</p>
    </div>
  \`
})
export class CounterComponent {
  // Signal updates automatically notify the template
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  increment() {
    this.count.update(v => v + 1);
    // No markForCheck() needed - signal handles notification
  }

  decrement() {
    this.count.update(v => v - 1);
  }
}

Oltre ai Signals, nella modalità Zoneless la change detection viene attivata anche dai seguenti meccanismi:

  • Template event bindings: ogni handler (click), (input) o qualsiasi altro evento nel template attiva un ciclo di change detection dopo la propria esecuzione.
  • ChangeDetectorRef.markForCheck(): contrassegna esplicitamente un componente per la verifica nel ciclo successivo.
  • Async Pipe: la pipe async invoca internamente markForCheck() quando un Observable emette un nuovo valore.
  • ComponentRef.setInput(): l'impostazione di Input properties tramite l'API ComponentRef attiva anch'essa la change detection.
OnPush e Zoneless

I componenti con changeDetection: ChangeDetectionStrategy.OnPush si comportano in modalità Zoneless in modo identico a quelli con strategia Default. In assenza di Zone.js non avviene alcuna verifica automatica dell'intero albero, pertanto la strategia Default è di fatto equivalente a OnPush. Le applicazioni esistenti che già utilizzano sistematicamente OnPush beneficiano di una migrazione particolarmente agevole verso la modalità Zoneless.

Insidie della migrazione: setTimeout, Reactive Forms e codice di terze parti

La fonte di errori più frequente nella migrazione a Zoneless risiede nel codice che dipende implicitamente da Zone.js per propagare le modifiche di stato. Ogni setTimeout, setInterval o modifica di stato basata su Promise che non transita attraverso Signals o chiamate esplicite alla change detection non viene riflessa automaticamente nella view in modalità Zoneless.

user-status.component.ts - Before: relies on Zone.jstypescript
@Component({
  selector: 'app-user-status',
  template: \`<span>{{ statusMessage }}</span>\`
})
export class UserStatusComponent {
  statusMessage = 'Loading...';

  ngOnInit() {
    setTimeout(() => {
      // Zone.js would trigger CD here - zoneless does NOT
      this.statusMessage = 'Ready';
    }, 2000);
  }
}

La soluzione consiste nel convertire lo stato in Signals, che notificano automaticamente Angular della modifica avvenuta:

user-status.component.ts - After: signal-based approachtypescript
import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-user-status',
  template: \`<span>{{ statusMessage() }}</span>\`
})
export class UserStatusComponent {
  statusMessage = signal('Loading...');

  ngOnInit() {
    setTimeout(() => {
      // Signal update notifies Angular automatically
      this.statusMessage.set('Ready');
    }, 2000);
  }
}

Un altro ambito problematico riguarda i Reactive Forms. Gli Observable valueChanges e statusChanges di FormControl, FormGroup e FormArray emettono valori in modo asincrono. In modalità Zoneless occorre assicurarsi che questi Observable vengano collegati alla view tramite la pipe async oppure convertiti in Signals mediante toSignal():

search.component.ts - Reactive forms with zonelesstypescript
import { Component, inject, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs';

@Component({
  selector: 'app-search',
  imports: [ReactiveFormsModule],
  template: \`
    <input [formControl]="searchControl" placeholder="Search..." />
    <p>Results for: {{ searchTerm() }}</p>
  \`
})
export class SearchComponent {
  searchControl = new FormControl('');

  // Convert observable to signal for automatic template updates
  searchTerm = toSignal(
    this.searchControl.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged()
    ),
    { initialValue: '' }
  );
}

L'utility toSignal() dal pacchetto @angular/core/rxjs-interop rappresenta il ponte ideale tra il mondo RxJS e quello dei Signals: converte qualsiasi Observable in un Signal, garantendo che le emissioni vengano riflesse automaticamente nel template senza necessità di Zone.js.

Pronto a superare i tuoi colloqui su Angular?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

SSR senza Zone.js: PendingTasks e stabilità dell'applicazione

Nel contesto del Server-Side Rendering, Zone.js svolge una funzione critica: segnala ad Angular quando tutte le operazioni asincrone sono completate e la pagina è "stabile", ossia pronta per la serializzazione e l'invio al client. Senza Zone.js, questa responsabilità viene assunta dal servizio PendingTasks.

Il servizio PendingTasks consente di registrare esplicitamente operazioni asincrone e contrassegnarle come completate. Angular attende la conclusione di tutte le task registrate prima di serializzare l'output SSR.

data-loader.component.ts - SSR-compatible async loadingtypescript
import { Component, inject, signal } from '@angular/core';
import { PendingTasks } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

@Component({
  selector: 'app-data-loader',
  template: \`
    @if (data()) {
      <div>{{ data()!.title }}</div>
    } @else {
      <div>Loading...</div>
    }
  \`
})
export class DataLoaderComponent {
  private http = inject(HttpClient);
  private pendingTasks = inject(PendingTasks);
  data = signal<{ title: string } | null>(null);

  ngOnInit() {
    // PendingTasks.run() prevents SSR serialization until complete
    this.pendingTasks.run(async () => {
      const result = await firstValueFrom(
        this.http.get<{ title: string }>('/api/data')
      );
      this.data.set(result);
    });
  }
}

Senza la registrazione tramite PendingTasks, Angular serializzerebbe la pagina prima del completamento della richiesta HTTP, consegnando al client una pagina con il testo "Loading...". Il servizio PendingTasks sostituisce quindi il rilevamento implicito della stabilità fornito da Zone.js con un'alternativa esplicita e controllata.

Per le richieste effettuate tramite HttpClient configurato con provideHttpClient(), Angular registra automaticamente le pending tasks. L'utilizzo manuale di PendingTasks è necessario soltanto per operazioni asincrone personalizzate, come connessioni WebSocket, chiamate fetch native o inizializzazioni basate su timer.

Benchmark prestazionali: Zone.js vs Zoneless

I dati seguenti illustrano le differenze misurabili tra un'applicazione Angular con Zone.js e la stessa applicazione dopo la migrazione alla Zoneless Change Detection.

| Metrica | Zone.js | Zoneless | Miglioramento | |---------|---------|----------|---------------| | Dimensione bundle iniziale | +33KB raw / +10KB gzip | 0KB overhead | Riduzione del 100% | | Cicli di change detection (app tipica) | 150-300 per interazione | 5-15 per interazione | 80-95% in meno | | Profondità stack trace | 8-12 frame Zone aggiuntivi | Trace native pulite | Chiarezza immediata | | Time to Interactive (TTI) | Baseline | 15-25% più veloce | Bootstrap Zone eliminato |

La riduzione dei cicli di change detection tra l'80% e il 95% deriva dalla verifica mirata: invece di attraversare l'intero albero dei componenti, la modalità Zoneless verifica esclusivamente i componenti effettivamente interessati dalla modifica. In una tabella con 1000 righe di cui solo 10 aggiornate, l'approccio Zoneless controlla unicamente le 10 righe modificate anziché tutte e 1000.

L'eliminazione del bootstrap di Zone.js si traduce in un miglioramento del Time to Interactive compreso tra il 15% e il 25%, particolarmente significativo su dispositivi mobili dove il parsing e l'esecuzione di JavaScript hanno un costo proporzionalmente maggiore. Le stack trace native, prive dei frame aggiuntivi di Zone.js, semplificano inoltre il debugging quotidiano in modo sostanziale.

Compatibilità delle librerie di terze parti

Non tutte le librerie Angular sono compatibili con la modalità Zoneless. Le librerie che dipendono internamente da Zone.js per attivare la change detection non funzionano correttamente senza adattamenti. Angular Material è completamente compatibile con Zoneless dalla versione 19. NgRx supporta Zoneless dalla versione 19. Per librerie meno diffuse, è opportuno consultare la documentazione specifica o l'issue tracker del progetto prima di procedere con la migrazione.

API di NgZone ancora disponibili dopo la migrazione

Anche dopo la transizione alla modalità Zoneless, alcune API di NgZone rimangono disponibili poiché svolgono un ruolo importante nella migrazione graduale. Il servizio NgZone può ancora essere iniettato, ma in modalità Zoneless si comporta come un'implementazione no-op.

NgZone.runOutsideAngular() è una tecnica di ottimizzazione diffusa che esegue codice al di fuori della Angular zone per evitare cicli di change detection superflui. In modalità Zoneless questa chiamata non produce alcun effetto, ma può rimanere nel codice senza causare errori. Questo agevola la migrazione, poiché il codice esistente non richiede una pulizia immediata.

NgZone.run() forza l'esecuzione all'interno della Angular zone. In modalità Zoneless esegue semplicemente il callback in modo sincrono. Anche questa API resta pienamente retrocompatibile.

Per il nuovo codice scritto in modalità Zoneless, queste API non sono più rilevanti. È preferibile affidarsi a Signals, ChangeDetectorRef.markForCheck() e al servizio PendingTasks per gestire la change detection in modo esplicito e prevedibile.

Percorso di migrazione: da Angular 19 sperimentale ad Angular 21 predefinito

L'introduzione della Zoneless Change Detection segue il consolidato schema di Angular per le modifiche architetturali significative: sperimentale, stabile, predefinito.

In Angular 19 provideExperimentalZonelessChangeDetection() è stata introdotta come Developer Preview. L'API poteva subire modifiche nelle release minori e il team Angular ne raccomandava l'uso esclusivamente in nuovi progetti o per test esplorativi.

Con Angular 20 l'API è stata dichiarata stabile con il nome provideZonelessChangeDetection(). Il comando ng new offre Zoneless come opzione di configurazione e la documentazione ufficiale raccomanda questa modalità per tutti i nuovi progetti. Le applicazioni esistenti possono migrare gradualmente.

In Angular 21, atteso per l'autunno 2026, ng new genererà progetti senza Zone.js per impostazione predefinita. Zone.js resterà disponibile come dipendenza opzionale per i progetti che la richiedono esplicitamente, ma non verrà più installata automaticamente.

Per la migrazione dei progetti esistenti, il team Angular raccomanda un approccio a fasi:

  1. Convertire tutti i componenti a ChangeDetectionStrategy.OnPush
  2. Sostituire le variabili di stato nei template con Signals
  3. Attivare provideZonelessChangeDetection() mantenendo Zone.js nel bundle
  4. Eseguire test funzionali e risolvere le anomalie riscontrate
  5. Rimuovere Zone.js dai polyfill e disinstallare il pacchetto

Questo piano a tappe minimizza il rischio e consente di identificare i problemi in anticipo, dato che Angular con il provider Zoneless attivo e Zone.js ancora presente nel bundle emette avvisi quando rileva change detection attivata tramite Zone.js anziché tramite Signals.

Conclusione

La Zoneless Change Detection rappresenta un passaggio architetturale fondamentale per Angular. I punti chiave si possono sintetizzare come segue:

  • Zone.js intercetta tutte le API asincrone del browser e attiva un ciclo completo di change detection alla conclusione di ogni operazione, generando un overhead significativo nelle applicazioni di grandi dimensioni
  • provideZonelessChangeDetection() abilita la modalità Zoneless ed elimina completamente la dipendenza da Zone.js
  • I Signals costituiscono il fondamento dell'architettura Zoneless, consentendo una change detection mirata e precisa a livello di singolo componente anziché una verifica dell'intero albero
  • Il codice esistente basato su setTimeout, setInterval o modifiche di stato tramite Promise deve essere convertito a Signals o a chiamate esplicite di markForCheck()
  • I Reactive Forms richiedono attenzione specifica: gli Observable valueChanges devono essere collegati alla view tramite la pipe async o convertiti in Signals con toSignal()
  • Le applicazioni SSR utilizzano il servizio PendingTasks in sostituzione del rilevamento implicito della stabilità fornito da Zone.js
  • I miglioramenti prestazionali variano dal 15-25% in fase di avvio fino all'80-95% nella riduzione dei cicli di change detection
  • La migrazione può avvenire in modo incrementale, poiché Zone.js e la modalità Zoneless possono coesistere nello stesso progetto
  • A partire da Angular 21 la modalità Zoneless sarà lo standard per i nuovi progetti, rendendo la padronanza di questa architettura indispensabile sia per le Top 25 domande colloquio Angular sia per il lavoro quotidiano con il modulo Angular Signals

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#angular
#zoneless
#change-detection
#performance
#signals
#zone-js

Condividi

Articoli correlati