<link rel="stylesheet" href="/assets/fonts/jetbrains-mono/jetbrains-mono.css" />
All posts

Angular Signals vs Zone.js: Perché Angular sta diventando Zoneless

Per oltre un decennio, Angular ha risolto un problema fondamentale — quando ricontrollare la UI dopo che qualcosa è cambiato — con una soluzione tanto ingegnosa quanto invasiva: Zone.js, una libreria che riscrive a runtime le API asincrone del browser per intercettare ogni possibile causa di cambiamento. Con l'arrivo dei Signals, Angular sta abbandonando gradualmente questo approccio a favore di un modello dove è il dato stesso, non un monkey-patch globale, a sapere chi deve essere aggiornato. Questo articolo spiega come funziona Zone.js internamente, perché il suo modello ha limiti strutturali non risolvibili, e cosa comporta in pratica rendere un'applicazione Angular zoneless.

Come funziona il change detection basato su Zone.js

Zone.js risolve un problema molto concreto: Angular deve sapere quando ricontrollare i componenti per aggiornare il DOM, ma JavaScript non offre nativamente un modo per "osservare" quando un valore cambia in modo generico. La soluzione di Zone.js è drastica: al bootstrap dell'applicazione, sovrascrive (monkey-patch) le API asincrone globali del browser — setTimeout, setInterval, addEventListener, Promise, XMLHttpRequest — in modo che ogni volta che una di queste API completa un'operazione, Zone.js lo intercetta e notifica Angular.

Il ciclo di change detection "a tappeto"

Quando Zone.js notifica un evento, Angular non sa cosa esattamente è cambiato — sa solo che qualcosa, da qualche parte, potrebbe essere cambiato. La risposta è ricontrollare l'intero albero dei componenti dall'alto verso il basso (dirty checking), confrontando i valori nei template con quelli renderizzati in precedenza:

// Semplificato: cosa succede concettualmente dopo OGNI evento asincrono
zone.onMicrotaskEmpty.subscribe(() => {
  applicationRef.tick(); // ricontrolla l'intero albero dei componenti
});

Questo è il motivo per cui, storicamente, un singolo click su un bottone in un angolo dell'applicazione può far ricontrollare componenti completamente estranei altrove nell'albero — ChangeDetectionStrategy.OnPush mitiga il problema (salta i sottoalberi i cui @Input() non sono cambiati per riferimento), ma non lo elimina: Zone.js continua comunque a triggerare un ciclo di controllo ad ogni singolo evento asincrono, che porti o meno a un cambiamento reale.

I limiti strutturali di Zone.js

Zone.js ha funzionato bene come soluzione "buona per tutti i casi", ma il suo approccio ha problemi che non si possono risolvere con ottimizzazioni incrementali:

1. Overhead di bundle e di runtime

Zone.js pesa circa 30-35KB (minified) nel bundle iniziale — un costo fisso per ogni applicazione Angular, indipendentemente da quanto ne sfrutti effettivamente le funzionalità. A runtime, il monkey-patching di ogni API asincrona introduce un overhead misurabile su ogni singola chiamata, anche quando non porta a nessun cambiamento reale nella UI.

2. Change detection non selettivo

Zone.js sa che qualcosa è successo, ma non sa cosa. Il risultato è un ciclo di controllo che nella maggior parte dei casi ricontrolla molto più di quanto sia necessario — anche con OnPush attivo ovunque, ogni evento asincrono innesca comunque un giro di verifica sull'intero albero potenzialmente coinvolto.

3. Fragilità con librerie di terze parti

Il monkey-patching globale di API come Promise o fetch può interagire in modo imprevedibile con librerie che non si aspettano che queste API vengano riscritte a runtime — è una causa comune e difficile da diagnosticare di bug intermittenti in applicazioni Angular di grandi dimensioni.

4. Debug più complesso

Gli stack trace attraversano il codice di patching di Zone.js, rendendo più difficile risalire all'origine reale di un errore asincrono — chiunque abbia debuggato una UnhandledPromiseRejection in un'app Angular grande conosce questo problema.

Cosa cambiano i Signals

I Signals invertono il modello: invece di "qualcosa è successo da qualche parte, ricontrolla tutto", il dato stesso sa esattamente chi dipende da lui, perché la dipendenza viene registrata esplicitamente al momento della lettura:

import { signal, computed, effect } from '@angular/core';

const count = signal(0);
const doubled = computed(() => count() * 2); // dipendenza tracciata automaticamente

effect(() => {
  console.log('Il valore doppio è', doubled());
  // questo effect si ri-esegue SOLO quando doubled() cambia realmente,
  // non ad ogni evento asincrono dell'applicazione
});

count.set(5); // notifica solo i consumer reali: doubled, e l'effect

Quando count.set(5) viene chiamato, Angular sa con precisione quali computed, quali effect e quali binding di template dipendono da count — non serve ricontrollare l'intero albero dei componenti, perché il grafo delle dipendenze è già noto in anticipo.

Zone.js vs Signals: confronto diretto

AspettoZone.js (change detection classico)Signals
Come rileva i cambiamentiIntercetta ogni API asincrona globaleTracciamento esplicito delle dipendenze alla lettura
Cosa ricontrollaL'intero albero (o sottoalbero con OnPush)Solo i consumer effettivamente dipendenti
Overhead di bundle~30-35KB fissiIncluso nel core, nessun costo aggiuntivo
Compatibilità con librerie terzeRischio di conflitti da monkey-patchingNessun patching globale necessario
Prevedibilità del debugStack trace attraversano il patchingFlusso di dipendenze esplicito e tracciabile
Curva di adozioneFunziona "gratis" fin dal primo giornoRichiede refactoring dello stato esistente

Cosa significa "Angular zoneless" in pratica

Angular zoneless non significa semplicemente "usare i Signals nei componenti" — significa rimuovere del tutto Zone.js dal bundle e affidare l'intero change detection al grafo di reattività dei Signals. Si attiva con un provider dedicato in fase di bootstrap:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [
    provideZonelessChangeDetection(),
    // ...altri provider
  ],
});

Una volta rimosso Zone.js, Angular non ha più alcun modo "automatico" di accorgersi di un cambiamento generico — ogni stato che deve riflettersi nella UI deve passare esplicitamente da un Signal (o da un ChangeDetectorRef.markForCheck() manuale nei casi limite). Questo è il motivo per cui la migrazione non è un semplice flag da attivare, ma un vero e proprio cambio di paradigma nella gestione dello stato.

Guida pratica: migrare un'applicazione verso zoneless

1. Verifica la copertura di OnPush

Se l'applicazione usa ancora ChangeDetectionStrategy.Default in molti componenti, è il primo segnale che lo stato non è gestito in modo esplicito — passare tutto a OnPush è un prerequisito pratico prima ancora di rimuovere Zone.js.

2. Sostituisci lo stato "implicito" con Signals

// Prima: proprietà di classe normale, aggiornata via mutazione diretta
export class CartComponent {
  itemCount = 0;
  addItem(): void {
    this.itemCount++; // senza Zone.js, la UI non si aggiornerebbe
  }
}

// Dopo: signal, aggiornamento esplicito e tracciato
export class CartComponent {
  itemCount = signal(0);
  addItem(): void {
    this.itemCount.update(n => n + 1); // il template si aggiorna automaticamente
  }
}

3. Verifica le librerie di terze parti

Alcune librerie (specialmente componenti di terze parti più datati, o codice che assume implicitamente la presenza di Zone.js per "svegliare" Angular dopo un evento) potrebbero smettere di aggiornare la UI correttamente in modalità zoneless. Vanno testate singolarmente, o avvolte in un markForCheck() manuale nei punti di integrazione.

4. Rimuovi Zone.js dal bundle

Una volta verificato che tutto lo stato passa da Signals (o da change detection manuale esplicita), Zone.js può essere rimosso dalle dipendenze e dal file di polyfill, riducendo il bundle iniziale.

Quando NON conviene ancora andare zoneless

  • Applicazioni con molte dipendenze di terze parti datate: se librerie critiche assumono la presenza di Zone.js, la migrazione richiede verifiche estese caso per caso.
  • Codebase molto grandi con stato gestito in modo non uniforme: se lo stato è sparso tra proprietà di classe mutate direttamente, servizi con RxJS non integrati con i Signals, e logica legacy, il refactoring necessario è sostanziale.
  • Team senza familiarità con i Signals: zoneless amplifica qualsiasi lacuna nella gestione esplicita dello stato — vale la pena consolidare prima la conoscenza dei Signals con Zone.js ancora attivo come rete di sicurezza.

Domande frequenti

Devo rimuovere Zone.js per usare i Signals?

No, i Signals funzionano perfettamente anche con Zone.js ancora attivo — è il passo intermedio più comune: prima si adottano i Signals per lo stato, poi (opzionalmente) si rimuove Zone.js quando la copertura è completa.

I Signals sostituiscono RxJS in Angular?

No, coprono casi d'uso diversi: i Signals sono pensati per stato sincrono locale ai componenti, RxJS resta lo strumento giusto per flussi asincroni complessi (richieste HTTP, WebSocket, combinazione di più eventi nel tempo). Le utility toSignal() e toObservable() permettono di far convivere i due modelli.

Zoneless è già pronto per la produzione?

Il supporto zoneless è stabile nelle versioni recenti di Angular, ma richiede che l'intera applicazione (incluse le librerie di terze parti in uso) sia compatibile con un modello di change detection esplicito — va valutato caso per caso, non è un semplice switch universale.

Cosa succede se dimentico di usare un Signal per uno stato che deve aggiornare la UI?

In modalità zoneless, la UI semplicemente non si aggiorna finché non avviene un altro trigger di change detection (es. un evento del template) — è un bug silenzioso, motivo per cui la migrazione richiede test approfonditi su ogni flusso.

I Signals migliorano le performance anche senza andare zoneless?

Sì: anche con Zone.js ancora attivo, i binding basati su Signals nei template permettono ad Angular di aggiornare solo i nodi DOM effettivamente dipendenti, riducendo il lavoro di rendering rispetto ai binding classici.

Serve riscrivere tutta l'applicazione per adottare i Signals?

No, Signals e stato "classico" possono coesistere nello stesso componente e nella stessa applicazione — è possibile migrare in modo incrementale, componente per componente.


In sintesi

Zone.js ha permesso ad Angular di offrire change detection automatico "gratis" fin dal primo giorno, ma il prezzo è un modello che ricontrolla molto più di quanto serva, con un costo fisso di bundle e una fragilità nota nell'integrazione con librerie di terze parti. I Signals risolvono il problema alla radice, tracciando le dipendenze in modo esplicito invece di intercettare ogni evento asincrono del browser — e aprono la strada a un Angular zoneless, più leggero e più prevedibile da debuggare. Se stai valutando quando usare i Signals rispetto a RxJS nella gestione dello stato quotidiana, questa guida si collega direttamente a Angular Signals vs RxJS: quando usare l'uno e quando l'altro.

💬 Note dei lettori

1 nota

Scrivi una nota

Condividi la tua opinione, un suggerimento o un complimento

Ultime note

Matteo
l&#x27;ho letto con attenzione, un riassunto abbastanza preciso.