Zoneless Change Detection: Addio Zone.js
Con Angular 18 il team ha introdotto la possibilità sperimentale di eseguire applicazioni senza Zone.js, e con Angular 21 questa funzionalità e diventata stabile e pronta per la produzione. L'eliminazione di Zone.js rappresenta uno dei cambiamenti architetturali più significativi nella storia del framework, con impatti diretti su performance, dimensione del bundle e semplicità del modello mentale.
In questo secondo articolo della serie Angular Moderno vedremo in dettaglio cos'è Zone.js, perchè e stato un pilastro del framework per quasi un decennio, e come il nuovo modello zoneless basato sui Signals lo rende finalmente superfluo. Dalla configurazione alla migrazione, passando per benchmark reali e pattern pratici, questa guida copre tutto ciò che serve per fare il salto.
Panoramica della Serie Angular Moderno
| # | Articolo | Focus |
|---|---|---|
| 1 | Angular Signals | Reattività fine-grained |
| 2 | Sei qui - Zoneless Change Detection | Eliminare Zone.js |
| 3 | Nuovi Template @if, @for, @defer | Control flow moderno |
| 4 | Standalone Components | Architettura senza NgModule |
| 5 | Signal Forms | Form reattivi con Signals |
| 6 | SSR e Incremental Hydration | Server-Side Rendering |
| 7 | Core Web Vitals in Angular | Performance e metriche |
| 8 | Angular PWA | Progressive Web App |
| 9 | Dependency Injection Avanzata | DI tree-shakable |
| 10 | Migrare da Angular 17 a 21 | Guida alla migrazione |
Cos'è Zone.js e perchè è un Problema
Zone.js è una libreria che Angular ha utilizzato fin dalla versione 2 come meccanismo fondamentale per il change detection. Il suo compito è semplice in teoria ma invasivo in pratica: intercettare tutte le operazioni asincrone del browser per sapere quando potrebbe essere necessario aggiornare la vista.
Il Monkey-Patching Globale
Zone.js funziona attraverso una tecnica chiamata monkey-patching: al caricamento
dell'applicazione, sostituisce le implementazioni native delle API asincrone del browser
con le proprie versioni "wrappate". Questo significa che ogni chiamata a
setTimeout, setInterval, Promise,
addEventListener, XMLHttpRequest, fetch,
requestAnimationFrame e molte altre API viene intercettata.
// Questo è ciò che Zone.js fa internamente (semplificato)
const originalSetTimeout = window.setTimeout;
window.setTimeout = function(callback: Function, delay: number) {
// Notifica la zona corrente che sta iniziando un task asincrono
const zone = Zone.current;
zone.onScheduleTask('setTimeout');
return originalSetTimeout(() => {
zone.onInvokeTask('setTimeout');
callback();
// Dopo l'esecuzione, Angular sa che qualcosa potrebbe essere cambiato
zone.onHasTask(); // Trigger change detection!
}, delay);
};
// Lo stesso avviene per Promise, addEventListener,
// fetch, XMLHttpRequest, MutationObserver, ecc.
I Problemi Concreti di Zone.js
- Bundle size: Zone.js aggiunge circa 100KB+ non compressi (circa 13KB gzip) al bundle dell'applicazione
- Overhead runtime: Ogni operazione asincrona passa attraverso un layer aggiuntivo, anche quando non modifica lo stato dell'applicazione
- Change detection eccessiva: Un semplice
setTimeoutusato per un'animazione scatena un ciclo completo di change detection su tutti i componenti - Incompatibilita con API native: Web Components,
async/awaitnativo,queueMicrotaske alcune librerie di terze parti non funzionano correttamente con Zone.js - Debugging inquinato: Gli stack trace contengono decine di frame interni di Zone.js, rendendo quasi impossibile seguire il flusso di esecuzione
- Non tree-shakable: Anche se la tua app non usa
setTimeout, Zone.js patcha comunque tutte le API
Per visualizzare l'impatto, consideriamo un esempio: un'applicazione con un componente
che mostra un orologio aggiornato ogni secondo. Con Zone.js, il setInterval
scatena un ciclo di change detection sull'intero albero dei componenti
ad ogni tick, anche se solo un singolo binding nel template deve essere aggiornato.
Come Funziona il Change Detection Tradizionale
Per comprendere il modello zoneless, è essenziale capire come funziona il change detection tradizionale basato su Zone.js. Il processo si articola in diversi passaggi.
Il Ciclo di Change Detection con Zone.js
Quando un evento asincrono viene intercettato, Zone.js notifica Angular attraverso
NgZone, che a sua volta chiama ApplicationRef.tick().
Questo metodo avvia un dirty checking top-down dell'intero albero
dei componenti.
// 1. L'utente clicca un bottone
// Zone.js intercetta addEventListener
//
// 2. Il callback viene eseguito dentro la NgZone
// NgZone.onMicrotaskEmpty$.emit()
//
// 3. ApplicationRef ascolta e trigghera il tick
class ApplicationRef {
tick(): void {
// Controlla OGNI componente nell'albero
for (const view of this._views) {
view.detectChanges(); // Dirty checking top-down
}
}
}
// 4. Ogni componente confronta i valori correnti con quelli precedenti
class ComponentView {
detectChanges(): void {
// Per OGNI binding nel template:
// if (currentValue !== previousValue) {
// updateDOM();
// }
// Poi controlla tutti i componenti figli...
}
}
Flusso Zone.js vs Zoneless a Confronto
| Fase | Con Zone.js | Zoneless (Signals) |
|---|---|---|
| Intercettazione evento | Zone.js patcha l'API | Nessuna intercettazione |
| Notifica al framework | NgZone emette evento | Il Signal notifica i consumatori |
| Scope del check | Intero albero componenti | Solo i componenti che leggono il Signal |
| Strategia | Dirty checking (confronto valori) | Push-based (notifica diretta) |
| Componenti controllati | Tutti (o tutti fino a OnPush) | Solo quelli marcati "dirty" |
| Trigger inutili | Frequenti (ogni async op) | Zero (solo su cambiamento reale) |
Il problema fondamentale è chiaro: con Zone.js, Angular non sa cosa è cambiato, sa solo che qualcosa potrebbe essere cambiato. Quindi deve controllare tutto. Con i Signals, Angular sa esattamente quale valore è cambiato e quali componenti ne dipendono.
Il Nuovo Modello Zoneless
Il modello zoneless elimina completamente Zone.js dall'equazione. Al suo posto, Angular
utilizza i Signals come meccanismo primario per sapere quando e dove
aggiornare il DOM. Introdotto come sperimentale in Angular 18 con
provideExperimentalZonelessChangeDetection(), è diventato stabile e
ufficiale in Angular 21 con provideZonelessChangeDetection().
Come i Signals Guidano il Change Detection
Nel modello zoneless, il ciclo di aggiornamento funziona in modo completamente diverso. Quando un Signal cambia valore, notifica automaticamente Angular che il componente che lo legge nel template è "dirty" e deve essere riesaminato. Solo quel componente (e i suoi discendenti, se necessario) vengono controllati.
// Con Zoneless, i Signals notificano direttamente Angular
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Contatore: {{ count() }}</p>
<button (click)="increment()">+1</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1);
// 1. Il Signal sa che il suo valore è cambiato
// 2. Angular marca SOLO questo componente come dirty
// 3. Al prossimo tick, SOLO questo componente viene controllato
// 4. Il binding {{ count() }} viene aggiornato nel DOM
// Nessun altro componente viene toccato!
}
}
Trigger del Change Detection in Modalità Zoneless
Senza Zone.js, Angular rileva i cambiamenti attraverso questi meccanismi:
- Signal changes: Qualsiasi modifica a un Signal letto nel template marca il componente come dirty
- Event binding: Gli handler degli eventi nel template (come
(click)) triggerano automaticamente un check dopo l'esecuzione - Async pipe: L'async pipe continua a funzionare perchè internamente chiama
markForCheck() - ChangeDetectorRef: Puoi forzare manualmente il check con
markForCheck()odetectChanges() - Template input bindings: I cambiamenti nei binding
[input]sono tracciati automaticamente
Configurare un'Applicazione Zoneless
Passare a un'applicazione zoneless richiede pochi ma precisi passaggi. Vediamoli uno per uno.
Step 1: Abilitare il Provider Zoneless
Il primo passo è aggiungere il provider provideZonelessChangeDetection()
nella configurazione dell'applicazione.
// app.config.ts
import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
// Sostituisce provideZoneChangeDetection()
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(),
]
};
Step 2: Rimuovere Zone.js dai Polyfills
Zone.js viene tipicamente caricato come polyfill. Devi rimuoverlo dal file
angular.json (o project.json per Nx).
{
"projects": {
"my-app": {
"architect": {
"build": {
"options": {
"polyfills": [
// RIMUOVI questa riga:
// "zone.js"
]
}
},
"test": {
"options": {
"polyfills": [
// RIMUOVI anche qui:
// "zone.js",
// "zone.js/testing"
]
}
}
}
}
}
}
Step 3: Disinstallare il Pacchetto Zone.js
# Rimuovi zone.js dalle dipendenze
npm uninstall zone.js
# Verifica che non ci siano import residui
grep -r "zone.js" src/ --include="*.ts"
# Se trovi import come questi, rimuovili:
# import 'zone.js';
# import 'zone.js/testing';
Verifica Importante
Dopo aver rimosso Zone.js, controlla che non ci siano riferimenti residui in:
src/polyfills.ts(se esiste ancora nel tuo progetto)src/main.ts- rimuovi eventuali import di zone.jssrc/test.tsosrc/test-setup.ts- File di test unitari che importano
zone.js/testing
Step 4: Aggiornare il Bootstrap (se necessario)
// main.ts - Nessun cambiamento necessario nella maggior parte dei casi
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));
// La differenza sta tutta in appConfig:
// provideZonelessChangeDetection() al posto di
// provideZoneChangeDetection()
Signal-based Change Detection in Profondità
Con il modello zoneless, i Signals diventano il cuore pulsante del change detection. Comprendere come Angular traccia le dipendenze e programma gli aggiornamenti e fondamentale per scrivere codice efficiente.
Il Grafo delle Dipendenze
Quando Angular renderizza un template che contiene {{ mySignal() }},
registra automaticamente una dipendenza tra quel binding e il Signal.
Quando il Signal cambia, il framework sa esattamente quale vista deve essere aggiornata.
@Component({
selector: 'app-user-profile',
standalone: true,
// ZONELESS: Angular traccia che questo template
// dipende da firstName(), lastName() e fullAddress()
template: `
<h2>{{ firstName() }} {{ lastName() }}</h2>
<p>{{ fullAddress() }}</p>
<button (click)="updateName('Marco')">Cambia nome</button>
`
})
export class UserProfileComponent {
firstName = signal('Federico');
lastName = signal('Calo');
city = signal('Bari');
street = signal('Via Roma 1');
// computed: dipende da city() e street()
fullAddress = computed(() =>
`${this.street()}, ${this.city()}`
);
updateName(name: string) {
this.firstName.set(name);
// Solo il binding {{ firstName() }} viene aggiornato
// lastName() e fullAddress() restano invariati
// Nessun altro componente nell'albero viene controllato
}
}
OnPush Automatico
In modalità zoneless, tutti i componenti si comportano implicitamente come se avessero
ChangeDetectionStrategy.OnPush. Questo significa che un componente viene
controllato solo quando:
- Un Signal letto nel suo template cambia valore
- Un input binding riceve un nuovo valore
- Un evento nel template viene gestito
- Viene chiamato manualmente
markForCheck()
Impatto sulle Strategie di Change Detection Esistenti
| Strategia | Con Zone.js | Zoneless |
|---|---|---|
Default |
Controlla sempre ad ogni tick | Equivale a OnPush (controlla solo se dirty) |
OnPush |
Controlla solo su input change, eventi, markForCheck | Comportamento identico |
markForCheck() |
Necessario per aggiornamenti asincroni manuali | Necessario solo per codice non-Signal |
Gestire il Codice Asincrono senza Zone.js
Senza Zone.js che intercetta automaticamente le operazioni asincrone, devi assicurarti che Angular venga notificato quando lo stato cambia. Se usi i Signals, questo avviene automaticamente. Ma per codice legacy o casi particolari, potresti dover intervenire manualmente.
Caso 1: setTimeout e setInterval
@Component({
selector: 'app-timer',
standalone: true,
template: `
<p>Con Signal: {{ countSignal() }}</p>
<p>Senza Signal: {{ countPlain }}</p>
`
})
export class TimerComponent {
// FUNZIONA: Il Signal notifica Angular automaticamente
countSignal = signal(0);
// NON FUNZIONA senza Zone.js: Angular non sa che è cambiato!
countPlain = 0;
private cdr = inject(ChangeDetectorRef);
constructor() {
// Approccio Signal (consigliato)
setInterval(() => {
this.countSignal.update(v => v + 1);
// Angular aggiorna automaticamente il template
}, 1000);
// Approccio senza Signal (richiede intervento manuale)
setInterval(() => {
this.countPlain++;
this.cdr.markForCheck(); // Necessario!
// Senza markForCheck, il template non si aggiorna
}, 1000);
}
}
Caso 2: Chiamate HTTP
@Component({
selector: 'app-data-loader',
standalone: true,
template: `
@if (loading()) {
<div class="spinner">Caricamento...</div>
} @else {
<ul>
@for (user of users(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
}
`
})
export class DataLoaderComponent {
private http = inject(HttpClient);
users = signal<User[]>([]);
loading = signal(true);
constructor() {
this.http.get<User[]>('/api/users').subscribe({
next: (data) => {
this.users.set(data); // Signal notifica Angular
this.loading.set(false); // Signal notifica Angular
// Nessun markForCheck necessario!
},
error: (err) => {
console.error('Errore:', err);
this.loading.set(false);
}
});
}
}
Caso 3: WebSocket e Stream Continui
@Injectable({ providedIn: 'root' })
export class WebSocketService {
private messages = signal<Message[]>([]);
readonly latestMessages = this.messages.asReadonly();
connect(url: string): void {
const ws = new WebSocket(url);
ws.onmessage = (event) => {
const msg: Message = JSON.parse(event.data);
// Il Signal notifica tutti i componenti che lo leggono
this.messages.update(msgs => [...msgs, msg]);
};
}
}
// Nel componente, basta leggere il Signal nel template:
@Component({
template: `
@for (msg of wsService.latestMessages(); track msg.id) {
<div class="message">{{ msg.text }}</div>
}
`
})
export class ChatComponent {
wsService = inject(WebSocketService);
}
La Regola d'Oro per il Codice Asincrono Zoneless
La regola è semplice: se aggiorni lo stato tramite un Signal, Angular lo sapra
automaticamente. Se aggiorni una proprietà semplice (non-Signal), devi chiamare
manualmente ChangeDetectorRef.markForCheck(). La soluzione migliore è
convertire tutte le proprietà reattive in Signals.
Pattern di Migrazione da Zone.js
Migrare un'applicazione esistente da Zone.js a zoneless è un processo che può essere affrontato in modo incrementale. Non è necessario riscrivere tutto in una volta: Angular supporta una fase di transizione dove il codice Signal e il codice legacy coesistono.
Strategia di Migrazione in 5 Fasi
Piano di Migrazione Incrementale
-
Fase 1 - Analisi: Identifica tutti i componenti che usano proprietà
semplici (non-Signal) per lo stato reattivo. Cerca pattern come
{{ property }}nei template -
Fase 2 - Converti lo stato locale: Trasforma le proprietà dei componenti
in Signals. Aggiorna i template aggiungendo
()alle letture -
Fase 3 - Converti i service: Migra i
BehaviorSubjecte le proprietà dei service in Signals conasReadonly() -
Fase 4 - Converti gli input/output: Sostituisci
@Input()coninput()e@Output()conoutput() -
Fase 5 - Rimuovi Zone.js: Una volta che tutti i componenti usano Signals,
attiva
provideZonelessChangeDetection()e rimuovi Zone.js
Schematics di Migrazione Automatica
Angular CLI fornisce schematics che automatizzano parte della migrazione. Questi strumenti analizzano il codice e applicano le trasformazioni necessarie.
# Migra @Input() a input()
ng generate @angular/core:signal-input-migration
# Migra @Output() a output()
ng generate @angular/core:output-migration
# Migra @ViewChild/@ViewChildren a viewChild()/viewChildren()
ng generate @angular/core:signal-queries-migration
# Verifica la compatibilità zoneless
# (controlla se ci sono pattern incompatibili)
ng generate @angular/core:zoneless-migration
Pattern di Convivenza durante la Migrazione
Durante la transizione, potresti avere componenti migrati e non migrati nella stessa applicazione. Ecco come gestire la convivenza.
@Component({
selector: 'app-hybrid',
standalone: true,
template: `
<!-- Signal: funziona perfettamente in zoneless -->
<p>Signal: {{ countSignal() }}</p>
<!-- Observable con async pipe: funziona in zoneless -->
<p>Observable: {{ data$ | async }}</p>
<!-- Proprietà semplice: NON si aggiorna automaticamente -->
<!-- Richiede markForCheck() per aggiornamenti asincroni -->
<p>Legacy: {{ legacyCount }}</p>
`
})
export class HybridComponent {
// Già migrato
countSignal = signal(0);
// Funziona grazie a async pipe
data$ = inject(HttpClient).get('/api/data');
// Da migrare: servira markForCheck() in zoneless
legacyCount = 0;
private cdr = inject(ChangeDetectorRef);
updateLegacy() {
setTimeout(() => {
this.legacyCount++;
this.cdr.markForCheck(); // Necessario senza Zone.js
}, 1000);
}
}
Checklist Pre-Migrazione
- Verifica che tutti i componenti con
ChangeDetectionStrategy.Defaultfunzionino con OnPush (e il comportamento di default in zoneless) - Cerca tutti gli utilizzi di
NgZone.runOutsideAngular()e valuta se sono ancora necessari - Identifica le librerie di terze parti che dipendono da Zone.js
- Assicurati che i test unitari non dipendano da
zone.js/testingper le async operation - Verifica che i resolver e i guard del router usino Signals o Observable (non proprietà semplici)
Performance Benchmarks
L'eliminazione di Zone.js porta benefici misurabili e significativi. Ecco i dati raccolti da benchmark reali su applicazioni Angular di diverse dimensioni.
Benchmark: Applicazione E-Commerce (50+ componenti)
| Metrica | Con Zone.js | Zoneless | Miglioramento |
|---|---|---|---|
| Bundle size (gzip) | 285 KB | 272 KB | -13 KB (-4.5%) |
| Bundle size (non compresso) | 1.2 MB | 1.1 MB | -100 KB+ (-8%) |
| Time to Interactive (TTI) | 2.8s | 2.3s | -500ms (-18%) |
| First Contentful Paint | 1.4s | 1.2s | -200ms (-14%) |
| CD cycles per interazione | 12-15 | 1-3 | -80% cicli |
| Memoria heap media | 45 MB | 38 MB | -7 MB (-15%) |
| Tempo di bootstrap | 320ms | 260ms | -60ms (-19%) |
I miglioramenti più significativi si osservano nelle applicazioni con molti componenti e frequenti operazioni asincrone. In un'app con centinaia di componenti e aggiornamenti real-time (es. dashboard con WebSocket), la riduzione dei cicli di change detection può superare il 90%.
Dove si Nota la Differenza
- Applicazioni grandi: Più componenti hai, maggiore è il beneficio di non controllare l'intero albero
- Real-time updates: Dashboard, chat, notifiche: ogni aggiornamento tocca solo i componenti interessati
- Animazioni: Le animazioni basate su
requestAnimationFramenon triggerano più change detection globale - Dispositivi mobili: La riduzione del bundle e dell'overhead CPU migliora sensibilmente l'esperienza su device con risorse limitate
- Startup time: Eliminare il monkey-patching di Zone.js durante il bootstrap velocizza il caricamento iniziale
Componenti e Librerie di Terze Parti
Una delle preoccupazioni principali nella migrazione zoneless riguarda la compatibilità con le librerie di terze parti. La buona notizia è che la maggior parte delle librerie Angular mature sono già compatibili o in fase di adattamento.
Stato di Compatibilità delle Librerie Principali
| Libreria | Compatibilità Zoneless | Note |
|---|---|---|
| Angular Material | Completa (v18+) | Supporto nativo con Signals |
| Angular CDK | Completa (v18+) | Overlay, drag-drop, virtual scroll compatibili |
| PrimeNG | Completa (v18+) | Componenti aggiornati per zoneless |
| NgRx | Completa (v18+) | SignalStore nativo, Store con selectSignal() |
| NGXS | Parziale | Richiede async pipe o markForCheck |
| ng-bootstrap | Parziale | Alcuni componenti richiedono markForCheck |
| Transloco | Completa | Pipe e direttive compatibili |
| Angular Fire | Completa | Observable-based, funziona con async pipe o toSignal |
Gestire Librerie Non Compatibili
Se una libreria di terze parti non è ancora compatibile con il modello zoneless, ci sono diverse strategie per gestire la situazione.
// Strategia 1: Wrapper Component con markForCheck
@Component({
selector: 'app-legacy-wrapper',
standalone: true,
template: `
<legacy-datepicker
[value]="selectedDate()"
(dateChange)="onDateChange($event)">
</legacy-datepicker>
<p>Data selezionata: {{ selectedDate() }}</p>
`
})
export class LegacyWrapperComponent {
selectedDate = signal('');
private cdr = inject(ChangeDetectorRef);
onDateChange(date: string) {
this.selectedDate.set(date);
// Forza l'aggiornamento se la libreria non
// trigghera il change detection automaticamente
this.cdr.markForCheck();
}
}
// Strategia 2: Usare afterNextRender per operazioni DOM
@Component({
selector: 'app-chart-wrapper',
standalone: true,
template: `<div #chartContainer></div>`
})
export class ChartWrapperComponent {
chartContainer = viewChild.required<ElementRef>('chartContainer');
data = input.required<number[]>();
constructor() {
// afterNextRender e zoneless-compatible
afterNextRender(() => {
this.initChart();
});
// Reagisci ai cambiamenti dei dati
effect(() => {
const newData = this.data();
const container = this.chartContainer();
if (container) {
this.updateChart(container.nativeElement, newData);
}
});
}
private initChart() { /* ... */ }
private updateChart(el: HTMLElement, data: number[]) { /* ... */ }
}
Best Practices per il Modello Zoneless
Adottare il modello zoneless non si limita a rimuovere Zone.js: richiede un cambiamento nel modo di pensare la reattività dei componenti. Ecco le best practices consolidate dalla community e dal team Angular.
Le 10 Best Practices per Angular Zoneless
-
Tutto lo stato reattivo deve essere un Signal: Se una proprietà
appare nel template, deve essere un
signal()o uncomputed(). Le proprietà semplici non triggerano aggiornamenti -
Usa
computed()per lo stato derivato: Non duplicare stato calcolabile. Se un valore dipende da altri Signals, e un computed -
Preferisci
input()eoutput(): I decoratori legacy@Input()e@Output()funzionano ma non sono ottimizzati per il modello Signal -
Evita
NgZone: Se usiNgZone.runOutsideAngular()per ottimizzazioni, non ne hai più bisogno in zoneless. Rimuovi questi pattern -
Usa
effect()per i side effects: localStorage, analytics, logging: tutto ciò che non è stato derivato va in un effect -
Testa senza
fakeAsync: In modalità zoneless,fakeAsyncetick()non funzionano. Usaasync/awaitefixture.whenStable() -
Incapsula lo stato nei service: Usa il pattern
private
WritableSignal+ publicasReadonly()per impedire modifiche non controllate -
Sfrutta
toSignal()per gli Observable: Converti gli Observable HTTP e di servizio in Signals per usarli direttamente nel template -
Usa
afterNextRenderper il DOM: Al posto dingAfterViewInitcon timeout, usaafterNextRender()che è nativo e zoneless-compatible - Monitora le performance: Usa Angular DevTools per verificare quanti componenti vengono controllati ad ogni ciclo. In zoneless, il numero dovrebbe essere minimo
Testing in Modalità Zoneless
I test richiedono alcune modifiche quando si opera senza Zone.js. Il cambiamento principale è l'abbandono delle utility basate su Zone.js testing.
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
describe('CounterComponent (Zoneless)', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CounterComponent],
providers: [
// Usa zoneless anche nei test
provideZonelessChangeDetection()
]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
});
it('should increment the counter', async () => {
// Verifica stato iniziale
expect(component.count()).toBe(0);
// Esegui azione
component.increment();
// Attendi che Angular processi i cambiamenti
await fixture.whenStable();
// Verifica il DOM aggiornato
const el = fixture.nativeElement.querySelector('p');
expect(el.textContent).toContain('1');
});
it('should update derived state', async () => {
component.count.set(5);
// In zoneless, fixture.detectChanges() funziona ancora
fixture.detectChanges();
expect(component.doubleCount()).toBe(10);
});
// NOTA: fakeAsync e tick() NON funzionano in zoneless
// Usa async/await al loro posto
it('should handle async operations', async () => {
component.loadData();
// Attendi il completamento delle operazioni async
await fixture.whenStable();
fixture.detectChanges();
expect(component.data().length).toBeGreaterThan(0);
});
});
Quando NON Andare Zoneless
Scenari in cui Zone.js Potrebbe Essere Ancora Necessario
- Codebase legacy molto grande: Se hai centinaia di componenti con proprietà semplici e non puoi migrarli tutti, mantenere Zone.js può essere la scelta pragmatica
- Dipendenza critica da librerie incompatibili: Se la tua app dipende fortemente da una libreria che non supporta zoneless e non ha alternative
- Team non pronto: La migrazione richiede che tutti gli sviluppatori comprendano il modello Signal. Se il team non e formato, migrare potrebbe introdurre bug
- Deadline stringenti: La migrazione richiede tempo per testing approfondito. Non forzarla prima di un rilascio critico
In tutti questi casi, la strategia migliore è pianificare una migrazione graduale: inizia convertendo i nuovi componenti in Signals e migra progressivamente quelli esistenti.
Riepilogo e Prossimi Passi
Il modello zoneless rappresenta la direzione futura di Angular. Eliminando Zone.js, le applicazioni diventano più leggere, più veloci e più facili da comprendere. Il change detection non è più un processo misterioso e globale, ma un meccanismo preciso e tracciabile guidato dai Signals.
Concetti Chiave di Questo Articolo
- Zone.js intercettava tutte le API asincrone con monkey-patching, causando overhead e change detection eccessiva
- Zoneless usa i Signals per un change detection push-based e fine-grained
- Configurazione:
provideZonelessChangeDetection()+ rimuovere zone.js dai polyfills - Codice asincrono: Aggiorna lo stato tramite Signals e Angular si aggiorna automaticamente
- Migrazione: Incrementale in 5 fasi, supportata da schematics automatici
- Performance: Riduzione del bundle (-13KB gzip), TTI migliore (-18%), meno cicli di CD (-80%)
- Librerie: Angular Material, PrimeNG, NgRx sono già compatibili. Per le altre, usa wrapper con
markForCheck() - Testing: Usa
async/awaitefixture.whenStable()al posto difakeAsync/tick
Nel prossimo articolo della serie esploreremo i nuovi template con @if, @for e @defer,
il nuovo control flow di Angular che sostituisce le direttive strutturali *ngIf,
*ngFor e *ngSwitch con una sintassi più leggibile, performante
e type-safe.
Domande frequenti
Cos'è Zone.js e perchè è un problema?
Zone.js è una libreria che Angular ha utilizzato fin dalla versione 2 come meccanismo fondamentale per il change detection. Il suo compito è semplice in teoria ma invasivo in pratica: intercettare tutte le operazioni asincrone del browser per sapere quando potrebbe essere necessario aggiornare la vista.
Come funziona il ciclo di change detection con Zone.js?
Quando un evento asincrono viene intercettato, Zone.js notifica Angular attraverso , che a sua volta chiama . Questo metodo avvia un dirty checking top-down dell'intero albero dei componenti.
Come funziona il modello zoneless e i Signals?
Nel modello zoneless, il ciclo di aggiornamento funziona in modo completamente diverso. Quando un Signal cambia valore, notifica automaticamente Angular che il componente che lo legge nel template è "dirty" e deve essere riesaminato.
Quali sono i benefici della migrazione a un'applicazione zoneless?
L'eliminazione di Zone.js porta benefici misurabili e significativi, come la riduzione dei cicli di change detection e l'aumento delle prestazioni.

Commenti
Caricamento commenti...
Accedi per commentare