← Retour au blog
AngularPerformanceChange DetectionZonelessOnPush

Performance : OnPush et zoneless Angular, la fin du change detection coûteux

Le modèle par défaut de détection de changements Angular—une traversée complète de l'arbre des composants à chaque événement—tue les performances sur mobile et les apps complexes. OnPush existe depuis des années, mais zoneless Angular (disponible en v18+) change la donne : il retire la couche Zone.js et laisse le développeur contrôler explicitement quand recalculer. Ensemble, ces deux outils permettent une performance prévisible et maîtrisée.

Pourquoi le change detection par défaut coûte cher

Angular utilise Zone.js pour intercepter les événements asynchrones (setTimeout, promises, événements DOM) et déclencher automatiquement le change detection. À chaque détection, Angular parcourt l'arbre complet des composants, exécute les getters de templates, et compare les valeurs précédentes aux nouvelles. Sur une app avec centaines de composants imbriqués, cela devient vite un goulot. Le pire : même si seul un composant en bas de l'arbre a changé, la racine relance le calcul pour tous ses enfants.

Le problème s'aggrave avec les appels réseau fréquents, les animations, ou les applications mobiles où le CPU est limité. Un tableau avec 1000 lignes qui se met à jour toutes les secondes ? Vous verrez des freezes, du jank, et une batterie qui fond. Zone.js ajoute aussi du poids au bundle (~36 KB minifiés) et ralentit le startup.

OnPush : le premier pas vers la maîtrise

OnPush dit à Angular : « ne recalcule ce composant que si ses inputs changent, ou si quelque chose à l'intérieur déclenche explicitement le change detection ». C'est un contrat : en échange, le composant doit être immutable—ses inputs ne doivent jamais muter, seulement être remplacées.

Concrètement, avec OnPush, si vous avez un composant de liste affichant un tableau de produits, Angular ne re-render que si vous remplacez le tableau entier ou l'objet d'entrée. Pas de parcours inutile de l'arbre. Mais ça demande de la discipline : pas de this.items.push(...) , obligé de faire this.items = [...this.items, newItem] .

@Component({
  selector: 'app-product-list',
  template: `<div>{{ items | json }}</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
  @Input() items: Product[] = [];
}

Quand utilisé correctement, OnPush réduit drastiquement le nombre de checks. Sur une app moyenne, c'est un gain de 30 à 60 % en temps de change detection. Mais il faut être conscient : les observables avec async pipe, les événements internes, ou les modifications directes de propriétés peuvent créer des bugs subtils si le composant n'est pas pensé de manière immutable.

Zoneless Angular : libérer le contrôle

Zoneless Angular retire Zone.js complètement. Plus d'interception magique. À la place, vous déclarez explicitement quand une action doit déclencher le change detection via ChangeDetectorRef.markForCheck() ou ChangeDetectionStrategy.OnPush couplé aux signals.

C'est un changement de paradigme. Avec zoneless, chaque événement que vous écoutez explicitement dans le template ou via un listener devient responsable de signaler un changement. Les signals (l'autre grande innovation Angular récente) s'intègrent naturellement : quand un signal change, les composants qui le lisent sont automatiquement marqués pour re-render.

@Component({
  selector: 'app-counter',
  template: `<button (click)="increment()">Count: {{ count() }}</button>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = signal(0);
  increment() {
    this.count.update(v => v + 1);
  }
}

Le gain immédiat : plus de Zone.js = bundle plus léger, startup plus rapide, et surtout une performance plus prévisible. Vous savez exactement quand le change detection s'exécute. Pas de surprise. Sur une app lourde avec beaucoup d'événements, c'est une réduction de 20 à 40 % du temps CPU global.

Combiner OnPush et zoneless : la formule gagnante

Quand vous utilisez zoneless avec OnPush et signals, vous obtenez une synchronisation fine-grained : chaque composant ne re-render que si ses données directes changent. Pas de parcours global de l'arbre. Pas d'interception coûteuse.

Un exemple concret : une dashboard avec graphiques en temps réel. Sans zoneless, chaque nouvelle donnée (venant d'un WebSocket) déclenche un check global. Avec zoneless + OnPush, seul le composant du graphique se met à jour, et seulement s'il écoute le signal correspondant. Les autres panneaux ne bougent pas.

La migration vers zoneless demande du travail : il faut auditer où vous comptiez sur l'interception automatique de Zone.js. Les timers, les promises non-awaited, les appels tiers qui ne triggent pas d'événement DOM explicite—tout ça doit être refondu. Mais c'est faisable progressivement. Angular 18+ permet de mixer zoneless et applications avec Zone.js dans la même plateforme.

Pièges courants et solutions

Le piège majeur : oublier que OnPush exige l'immutabilité. Vous faites this.user.name = 'John' , et rien ne se met à jour, parce que l'objet n'a pas changé de référence. Solution : utiliser des opérateurs immuables ou des libraries comme Immer. Avec zoneless, c'est encore plus critique, car il n'y a pas de filet de sécurité Zone.js qui rattraperait votre mutation.

Deuxième piège : oublier d'injecter ChangeDetectorRef quand vous en avez besoin. Certains patterns, comme forcer un check manuel dans une callback asynchrone, demandent cdr.markForCheck() . Sans zoneless, vous pouviez vous en sortir avec un timeout. Avec zoneless, c'est obligatoire.

Troisième piège : mélanger signals et observables sans penser au cycle de vie. Un observable qui émet après la destruction du composant peut causer des memory leaks. Utilisez takeUntilDestroyed() systématiquement.

Recommandations pratiques

Commencez par auditer vos composants : lesquels reçoivent beaucoup d'inputs ? Lesquels sont immuables ? Activez OnPush d'abord sur ceux-ci. Mesurez avec Lighthouse ou Chrome DevTools (onglet Performance). Une app bien optimisée verra des gains tangibles en First Contentful Paint et Interaction to Next Paint.

Pour zoneless, testez sur une branche ou un feature flag. Les bénéfices en performance brute sont réels, mais la courbe d'apprentissage existe. Documentez vos patterns : comment gérez-vous les événements asynchrones ? Comment triggez-vous le change detection manuellement ?

N'oubliez pas les signals. Ils sont le ciment qui rend zoneless vraiment efficace. Remplacez progressivement les BehaviorSubject par des signals, surtout pour l'état local des composants. Les signals sont plus légers, plus rapides, et parfaitement intégrés au modèle zoneless.

Conclusion

OnPush et zoneless ne sont pas des optimisations cosmétiques. Ce sont des changements architecturaux qui redéfinissent comment Angular gère la réactivité. OnPush vous force à penser immutabilité et à réduire le coût du change detection. Zoneless vous libère du fardeau de Zone.js et vous donne le contrôle total.

Pour une app moderne, performante, et maintenable, la combinaison est inévitable. Vous ne construirez pas une app vraiment rapide sur mobile sans comprendre et utiliser ces deux concepts. Commencez dès maintenant : activez OnPush sur vos composants feuilles, adoptez les signals pour l'état local, et explorez zoneless pour votre prochaine feature critique.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️