Salta al contenuto principale

2 - Zoneless Change Detection: addio Zone.js

Zoneless Change Detection: Addio Zone.js Con Angular 18 il team ha introdotto la possibilità sperimentale di eseguire applicazioni senza Zone.js , e con...

Cosa ne pensi di questo articolo?

2 - Zoneless Change Detection: addio Zone.js
Angular ModernoFoto:Bibek ghoshPexels

In Breve

Zoneless Change Detection: Addio Zone.js Con Angular 18 il team ha introdotto la possibilità sperimentale di eseguire applicazioni senza Zone.js , e con...

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
1Angular SignalsReattività fine-grained
2Sei qui - Zoneless Change DetectionEliminare Zone.js
3Nuovi Template @if, @for, @deferControl flow moderno
4Standalone ComponentsArchitettura senza NgModule
5Signal FormsForm reattivi con Signals
6SSR e Incremental HydrationServer-Side Rendering
7Core Web Vitals in AngularPerformance e metriche
8Angular PWAProgressive Web App
9Dependency Injection AvanzataDI tree-shakable
10Migrare da Angular 17 a 21Guida 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.

Come Zone.js patcha le API del browser (semplificato)

// 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 setTimeout usato per un'animazione scatena un ciclo completo di change detection su tutti i componenti
  • Incompatibilita con API native: Web Components, async/await nativo, queueMicrotask e 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.

Il flusso del change detection tradizionale

// 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.

Come funziona il change detection zoneless

// 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() o detectChanges()
  • 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 - Abilitare il change detection zoneless

// 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).

angular.json - Rimuovere zone.js

{
  "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

Rimuovere zone.js dal progetto

# 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.js
  • src/test.ts o src/test-setup.ts
  • File di test unitari che importano zone.js/testing

Step 4: Aggiornare il Bootstrap (se necessario)

main.ts - Bootstrap senza zone.js

// 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.

Come Angular traccia le dipendenze dei Signals nel template

@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

setTimeout con e senza Signals

@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

HTTP calls in modalità zoneless

@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

WebSocket con aggiornamento Signal

@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

  1. Fase 1 - Analisi: Identifica tutti i componenti che usano proprietà semplici (non-Signal) per lo stato reattivo. Cerca pattern come {{ property }} nei template
  2. Fase 2 - Converti lo stato locale: Trasforma le proprietà dei componenti in Signals. Aggiorna i template aggiungendo () alle letture
  3. Fase 3 - Converti i service: Migra i BehaviorSubject e le proprietà dei service in Signals con asReadonly()
  4. Fase 4 - Converti gli input/output: Sostituisci @Input() con input() e @Output() con output()
  5. 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.

Comandi di migrazione automatica

# 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.

Componente ibrido durante la migrazione

@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.Default funzionino 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/testing per 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 requestAnimationFrame non 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.

Strategie per librerie non compatibili

// 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

  1. Tutto lo stato reattivo deve essere un Signal: Se una proprietà appare nel template, deve essere un signal() o un computed(). Le proprietà semplici non triggerano aggiornamenti
  2. Usa computed() per lo stato derivato: Non duplicare stato calcolabile. Se un valore dipende da altri Signals, e un computed
  3. Preferisci input() e output(): I decoratori legacy @Input() e @Output() funzionano ma non sono ottimizzati per il modello Signal
  4. Evita NgZone: Se usi NgZone.runOutsideAngular() per ottimizzazioni, non ne hai più bisogno in zoneless. Rimuovi questi pattern
  5. Usa effect() per i side effects: localStorage, analytics, logging: tutto ciò che non è stato derivato va in un effect
  6. Testa senza fakeAsync: In modalità zoneless, fakeAsync e tick() non funzionano. Usa async/await e fixture.whenStable()
  7. Incapsula lo stato nei service: Usa il pattern private WritableSignal + public asReadonly() per impedire modifiche non controllate
  8. Sfrutta toSignal() per gli Observable: Converti gli Observable HTTP e di servizio in Signals per usarli direttamente nel template
  9. Usa afterNextRender per il DOM: Al posto di ngAfterViewInit con timeout, usa afterNextRender() che è nativo e zoneless-compatible
  10. 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.

Test di un componente zoneless

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/await e fixture.whenStable() al posto di fakeAsync/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.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti