Angular 19 Zoneless : Detection de Changements sans Zone.js et Gains de Performance

Guide complet sur la detection de changements sans Zone.js dans Angular 19 et 20. Configuration de provideZonelessChangeDetection, migration vers les signals, SSR sans Zone.js, benchmarks de performance et pieges a eviter lors de la migration.

Diagramme comparatif de la detection de changements Angular avec et sans Zone.js

La suppression de Zone.js represente l'un des changements architecturaux les plus attendus dans l'ecosysteme Angular. Depuis sa creation, Angular s'appuie sur cette bibliotheque pour detecter automatiquement les modifications d'etat et mettre a jour le DOM. Zone.js intercepte plus de 130 API asynchrones du navigateur -- setTimeout, Promise.then, addEventListener -- afin de declencher des cycles de detection de changements apres chaque operation asynchrone. Cette approche, bien que transparente pour les developpeurs, genere un cout considerable en termes de performance : un bundle supplementaire de 33 Ko, des cycles de detection superflus et des stack traces polluees par des frames intermediaires.

Angular 19 introduit le mode zoneless en version experimentale, permettant aux applications de fonctionner sans Zone.js en s'appuyant exclusivement sur les signals et les notifications explicites. Angular 20 stabilise cette API, et Angular 21 en fait le comportement par defaut pour les nouveaux projets. Cette transition marque un tournant dans la maniere dont Angular gere la reactivite et la detection de changements.

Chronologie du mode zoneless

Angular 19 propose provideExperimentalZonelessChangeDetection() en statut experimental. Angular 20 stabilise l'API sous le nom provideZonelessChangeDetection(). Angular 21, prevu pour fin 2026, configure le mode zoneless par defaut dans les nouveaux projets generes via le CLI. Les applications existantes conservent Zone.js jusqu'a une migration explicite.

Fonctionnement de la detection de changements avec Zone.js

Pour comprendre les benefices du mode zoneless, il convient d'analyser le mecanisme que Zone.js implemente. Au demarrage de l'application, Zone.js applique des patches (monkey-patching) sur les API asynchrones natives du navigateur. Chaque appel a setTimeout, chaque resolution de Promise, chaque evenement du DOM declenche une notification vers Angular, qui execute alors un cycle complet de detection de changements sur l'arbre de composants.

Dans une application typique, une simple interaction utilisateur -- un clic sur un bouton, par exemple -- peut provoquer entre 150 et 300 cycles de detection de changements. La majorite de ces cycles sont superflus : ils parcourent l'integralite de l'arbre de composants alors que seul un fragment du DOM necessite une mise a jour.

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
  ]
};

L'option eventCoalescing: true, introduite dans Angular 14, regroupe les evenements DOM qui surviennent dans le meme tour de la boucle d'evenements en un seul cycle de detection. Ce mecanisme reduit le nombre de cycles, mais ne resout pas le probleme fondamental : Zone.js continue d'intercepter l'ensemble des API asynchrones, y compris celles qui ne modifient aucun etat de l'interface.

Activer le mode zoneless dans Angular 19 et 20

La configuration du mode zoneless differe selon la version d'Angular utilisee. Dans Angular 19, l'API porte le prefixe Experimental pour signaler son statut. A partir d'Angular 20, le prefixe disparait et l'API devient stable.

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(),
  ]
};

Une fois le provider zoneless configure, Zone.js n'est plus necessaire dans l'application. Le polyfill peut etre supprime du fichier angular.json (cibles build et test), puis le package desinstalle :

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

La suppression de Zone.js elimine immediatement 33 Ko du bundle brut (environ 10 Ko apres compression gzip). Plus significatif encore, le navigateur n'execute plus le code de monkey-patching au demarrage, ce qui accelere le Time to Interactive de 15 a 25 % selon la complexite de l'application.

Declencheurs de la detection de changements en mode zoneless

Sans Zone.js, Angular ne surveille plus les operations asynchrones de maniere globale. La detection de changements repose desormais sur des mecanismes explicites. Les signals constituent le principal declencheur : toute modification d'un signal notifie automatiquement Angular que les templates qui le consomment doivent etre reevalues.

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);
  }
}

Dans cet exemple, la modification du signal count declenche une reevaluation ciblee du template du composant CounterComponent. Le signal computed doubled est automatiquement recalcule lorsque count change. Aucun cycle global de detection de changements n'est execute : Angular met a jour uniquement les bindings affectes.

Outre les signals, plusieurs autres mecanismes declenchent la detection de changements en mode zoneless :

  • Les evenements du template (bindings (click), (input), etc.)
  • Les AsyncPipe qui recoivent une nouvelle valeur d'un Observable
  • L'appel explicite a ChangeDetectorRef.markForCheck()
  • Les outputs de composants enfants
  • Le setInterval interne utilise par Angular pour les animations
OnPush et mode zoneless

La strategie ChangeDetectionStrategy.OnPush reste pertinente en mode zoneless. Elle restreint la detection de changements aux composants dont les inputs ont change ou qui ont ete explicitement marques. Combiner OnPush avec les signals produit le modele de reactivite le plus performant disponible dans Angular : les mises a jour du DOM sont limitees au strict minimum, sans aucun cycle superflu.

Pieges de migration : setTimeout et formulaires reactifs

La transition vers le mode zoneless expose des patterns de code qui fonctionnaient grace a Zone.js mais qui cessent de mettre a jour le DOM sans celui-ci. Le cas le plus frequent concerne les mutations de proprietes classiques a l'interieur de callbacks asynchrones.

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);
  }
}

Avec Zone.js, le callback de setTimeout declenchait automatiquement un cycle de detection de changements, et le template se mettait a jour. En mode zoneless, la mutation de statusMessage passe completement inapercue par Angular. Le template continue d'afficher "Loading..." indefiniment.

La solution consiste a convertir la propriete en signal :

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);
  }
}

Le second piege majeur concerne les formulaires reactifs. Les FormControl d'Angular emettent des modifications via des Observables, et non via des signals. En mode zoneless, les changements de valeur d'un FormControl ne declenchent pas la detection de changements a moins d'etre explicitement convertis en signals avec 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: '' }
  );
}

La fonction toSignal() du package @angular/core/rxjs-interop souscrit a l'Observable et met a jour un signal a chaque emission. Ce pattern represente la passerelle officielle entre RxJS et le systeme de signals d'Angular.

Prêt à réussir tes entretiens Angular ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

SSR sans Zone.js : PendingTasks et serialisation

Le rendu cote serveur (SSR) avec Angular Universal pose un defi specifique en mode zoneless. Sans Zone.js, Angular ne peut plus detecter automatiquement quand toutes les operations asynchrones sont terminees avant de serialiser le HTML. Le service PendingTasks comble cette lacune en offrant un mecanisme explicite pour signaler les taches en cours.

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);
    });
  }
}

La methode PendingTasks.run() enregistre une tache asynchrone aupres du framework. Angular attend la completion de toutes les taches enregistrees avant de serialiser le HTML cote serveur. Sans ce mecanisme, le SSR produirait un HTML contenant l'etat "Loading..." au lieu des donnees resoluees, ce qui degraderait a la fois le SEO et l'experience utilisateur au premier rendu.

Le HttpClient enregistre automatiquement ses requetes aupres de PendingTasks en contexte SSR. En revanche, les operations asynchrones manuelles (fetch, setTimeout, API tierces) doivent etre explicitement enveloppees dans PendingTasks.run().

Benchmarks de performance

Les mesures suivantes, realisees sur une application de taille moyenne comportant environ 200 composants et 50 routes, illustrent les gains concrets apportes par le mode zoneless :

| Metrique | Zone.js | Zoneless | Amelioration | |--------|---------|----------|-------------| | Taille du bundle initial | +33 Ko brut / +10 Ko gzip | 0 Ko supplementaire | Reduction de 100 % | | Cycles de detection (interaction type) | 150-300 par interaction | 5-15 par interaction | 80-95 % en moins | | Profondeur des stack traces | 8-12 frames Zone supplementaires | Traces natives propres | Clarte immediate | | Time to Interactive (TTI) | Reference | 15-25 % plus rapide | Bootstrap Zone elimine |

La reduction du nombre de cycles de detection constitue le gain le plus significatif. En mode zoneless avec des signals, seuls les composants dont l'etat a effectivement change sont reevalues, contre un parcours complet de l'arbre de composants avec Zone.js. Cette precision produit des interfaces plus fluides, particulierement sur les appareils mobiles ou les ressources CPU sont limitees.

Compatibilite des bibliotheques tierces

Certaines bibliotheques de l'ecosysteme Angular s'appuient encore sur Zone.js pour declencher la detection de changements. Avant de migrer vers le mode zoneless, il convient de verifier la compatibilite des dependances tierces. Angular Material et les composants CDK sont compatibles zoneless depuis Angular 19. NgRx fonctionne nativement avec les signals depuis la version 18. En revanche, certaines bibliotheques de graphiques ou de composants UI plus anciennes peuvent necessiter des adaptations. La commande ng update signale les incompatibilites connues.

API NgZone qui survivent en mode zoneless

Meme en mode zoneless, le service NgZone reste disponible dans Angular. Son comportement change cependant de maniere significative : NgZone.run() et NgZone.runOutsideAngular() deviennent des no-ops (operations sans effet). Le code qui les utilise continue de compiler et de s'executer, mais les callbacks ne beneficient plus du mecanisme de detection automatique de Zone.js.

Cette compatibilite descendante facilite la migration progressive. Les appels existants a NgZone.run() ne provoquent pas d'erreur mais n'ont plus aucun effet. Les developpeurs peuvent les supprimer progressivement, en les remplacant par des mises a jour de signals ou des appels explicites a ChangeDetectorRef.markForCheck().

Le pattern runOutsideAngular(), frequemment utilise pour executer des operations lourdes sans polluer la detection de changements (animations Canvas, WebSocket haute frequence), devient redondant en mode zoneless. Puisque les operations asynchrones ne declenchent plus la detection par defaut, toute execution se deroule deja "en dehors d'Angular".

Chronologie d'Angular 19 a 21 : la feuille de route du mode zoneless

L'adoption du mode zoneless suit une trajectoire progressive sur trois versions majeures d'Angular :

Angular 19 (novembre 2025) : introduction de provideExperimentalZonelessChangeDetection(). Les equipes qui souhaitent adopter le mode zoneless peuvent le faire en acceptant le risque de changements d'API dans les versions suivantes. Les composants Angular Material et CDK sont rendus compatibles. Le CLI affiche des avertissements pour les patterns de code incompatibles avec le mode zoneless.

Angular 20 (mai 2026) : stabilisation de l'API sous le nom provideZonelessChangeDetection(). Le prefixe Experimental disparait. Les outils de migration du CLI incluent des schematics pour convertir automatiquement les proprietes de composants en signals et envelopper les operations asynchrones dans PendingTasks.run(). La documentation officielle recommande le mode zoneless pour les nouveaux projets.

Angular 21 (novembre 2026) : le mode zoneless devient le comportement par defaut pour les projets generes via ng new. Zone.js reste disponible en option explicite (provideZoneChangeDetection()) pour les applications heritees qui ne peuvent pas migrer immediatement. Le bundle par defaut d'une application Angular neuve ne contient plus Zone.js.

Cette progression sur trois versions permet aux equipes de migrer a leur rythme. Les applications existantes ne sont jamais forcees de migrer : Zone.js restera supporte en tant qu'option explicite pour les cycles LTS.

Pour approfondir les concepts de detection de changements dans un contexte d'entretien technique, les ressources suivantes sur SharpSkill couvrent les questions les plus frequemment posees :

Conclusion

Le mode zoneless d'Angular 19 et 20 represente une evolution fondamentale dans la gestion de la reactivite du framework. Les points essentiels a retenir :

  • Zone.js intercepte plus de 130 API asynchrones du navigateur, generant des cycles de detection superflus et un surpoids de 33 Ko dans le bundle
  • Angular 19 propose le mode zoneless en experimental via provideExperimentalZonelessChangeDetection(), stabilise dans Angular 20 sous provideZonelessChangeDetection()
  • Les signals constituent le mecanisme principal de notification en mode zoneless : toute modification d'un signal declenche une mise a jour ciblee du template
  • Les proprietes classiques mutees dans des callbacks asynchrones (setTimeout, setInterval) cessent de mettre a jour le DOM sans Zone.js -- la conversion en signals est indispensable
  • Les formulaires reactifs necessitent l'utilisation de toSignal() pour convertir les Observables de valueChanges en signals compatibles avec le mode zoneless
  • Le service PendingTasks garantit la serialisation correcte du HTML en SSR en signalant les operations asynchrones en cours
  • Les benchmarks montrent une reduction de 80 a 95 % des cycles de detection de changements et un TTI ameliore de 15 a 25 %
  • Angular 21, prevu pour fin 2026, active le mode zoneless par defaut dans les nouveaux projets, tout en conservant Zone.js comme option explicite pour les applications existantes

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

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

Partager

Articles similaires