La Change Detection est le processus par lequel Angular met à jour le DOM quand les données changent. Deux grands changements récents modifient votre façon de travailler : en v21, les nouvelles applications sont zoneless (sans Zone.js), et en v22, les composants sont OnPush par défaut. Ensemble, cela signifie : Angular ne « devine » plus quand quelque chose a changé — c’est à vous de le lui dire, et les signals le font automatiquement.
- v21 : les nouveaux projets n’incluent plus
zone.js. La Change Detection est planifiée par les signals, les événements etmarkForCheck. - v22 : les composants sans
changeDetectionsontOnPush.ChangeDetectionStrategy.DefaultdevientEager(Default est déprécié). - ng update vers la v22 ajoute
Eagerautomatiquement là où c’est nécessaire, donc rien ne casse.
OnPush par défaut et Eager
// v22 : pas de `changeDetection` → OnPush
@Component({ selector: 'app-user-card', template: `...` })
export class UserCard {}
// Vous voulez l’ancien comportement (« vérifie-moi toujours ») ? Explicitement :
@Component({
selector: 'app-legacy-widget',
changeDetection: ChangeDetectionStrategy.Eager, // l’ancien Default (Default est déprécié)
template: `...`,
})
export class LegacyWidget {}Ce qui déclenche un rendu (zoneless)
Ce qui planifie la Change Detection dans une application zoneless :
✓ un signal lu dans le template change (signal.set/update, input(), resource, toSignal…)
✓ un écouteur d’événement dans le template ou l’hôte : (click), (input), host: { '(keydown)': … }
✓ la pipe async reçoit une nouvelle valeur
✓ ChangeDetectorRef.markForCheck()
✓ ComponentRef.setInput(), attachement / détachement de vues
✓ le routeur : les navigations
✗ setTimeout / setInterval / Promise.then qui modifient un champ simple
✗ subscribe() qui modifie un champ simple
✗ callbacks de bibliothèques tierces (WebSocket, Firebase, Maps) qui modifient des champs simples// ✗ Fonctionne avec Zone.js, NE fonctionne PAS en zoneless + OnPush
export class Clock {
now = new Date();
constructor() {
setInterval(() => (this.now = new Date()), 1000);
}
}
// ✓ La solution idiomatique : un signal
export class Clock {
readonly now = signal(new Date());
constructor() {
const id = setInterval(() => this.now.set(new Date()), 1000);
inject(DestroyRef).onDestroy(() => clearInterval(id));
}
}
// ✓ Pour les Observables : toSignal (ou la pipe async)
readonly user = toSignal(this.userService.user$);
// Solution de secours pour l’ancien code : markForCheck
this.socket.on('msg', (m) => {
this.messages.push(m);
this.cdr.markForCheck();
});Les règles d’OnPush
Un composant OnPush n’est vérifié que lorsque :
1. l’un de ses input() reçoit une NOUVELLE référence (comparée avec ===)
2. un événement se produit en lui ou dans ses enfants
3. un signal lu dans son template change
4. la pipe async de son template émet
5. quelqu’un appelle markForCheck() sur lui (ou sur un descendant)
Sinon, tout son sous-arbre est ignoré. C’est pourquoi OnPush + signals = rendus minimaux.Mutations et OnPush
OnPush compare les inputs par référence. Si vous mutez un objet sur place, l’enfant ne le sait pas. Exactement la même règle que pour les signals : l’immutabilité.
ChangeDetectorRef : quand vous en avez vraiment besoin
const cdr = inject(ChangeDetectorRef);
cdr.markForCheck(); // « vérifie-moi au prochain tour » (asynchrone, recommandé)
cdr.detectChanges(); // « vérifie-moi MAINTENANT, de façon synchrone » (rare : mesures immédiates, tests)
cdr.detach(); // retire le composant de la Change Detection (dashboards à mises à jour manuelles)
cdr.reattach();
// v22 : cdr.checkNoChanges() a été retiré de l’API publique — dans les tests, utilisez fixture.detectChanges()Migrer un projet existant
# ng update vers la v22 ajoute automatiquement ChangeDetectionStrategy.Eager aux composants
# qui n’avaient pas de changeDetection — le comportement de l’application reste identique.
ng update @angular/core@22 @angular/cli@22
# Ensuite, composant par composant : retirez Eager, déplacez l’état dans des signals, testez.
# Pour passer en zoneless sur un projet existant, Angular 21 a ajouté un outil d’analyse :
# onpush_zoneless_migration (via le serveur MCP de l’Angular CLI : ng mcp)Ce sont les signals qui disent à Angular quoi re-rendre : signal() et computed() et, plus en détail, Les Signals sous le capot. Ce qui ralentit une application, pas à pas : Performance — une checklist.
Quand OUI, quand NON
- Tout nouveau composant — c’est déjà le comportement par défaut.
- L’état vit dans des signals, des inputs, des resources.
- Grandes listes, dashboards, pages avec beaucoup de contenu.
Eager: ancien code, pas encore migré, qui modifie des champs simples depuis des callbacks.markForCheck: intégration avec des bibliothèques tierces qui ne connaissent pas les signals (mais il vaut mieux mettre la valeur dans un signal).detectChanges: rarement — tests ou mesures juste après un changement.
Pièges fréquents
- « Ça ne marche qu’après un clic quelque part » = une valeur simple modifiée de façon asynchrone. Déplacez-la dans un signal.
- Vous mutez un objet reçu par un input → l’enfant OnPush ne se met pas à jour. Créez une nouvelle référence.
- Des tests qui reposaient sur Zone.js (
fakeAsync,whenStableimplicite) : en zoneless, utilisezawait fixture.whenStable()et les fake timers de Vitest. - Des bibliothèques anciennes qui supposent Zone.js (p. ex. celles qui attendent
NgZone.onStable) — vérifiez la compatibilité avant de retirer zone.js.