Lecture : 13 minutes · Niveau : avancé · Mise à jour : avril 2026
Une application Angular peut être très rapide ou très lente selon les choix de structure et de change detection. Avec les évolutions 2024-2026 (signals, zoneless, hydration), beaucoup d’optimisations qu’on faisait manuellement deviennent automatiques. Ce guide rassemble la démarche pratique pour mesurer, identifier et corriger les problèmes de performance, en distinguant ce qui compte vraiment de ce qui est cargo cult.
Voir aussi → Angular pour entreprise : guide pratique.
Sommaire
- Mesurer avant d’optimiser
- Change detection : modes et stratégies
- OnPush et signals : combo gagnant
- Bundle size et lazy loading
- Optimisation des images
- Server-Side Rendering et hydration
- Listes virtualisées
- Web Vitals et expérience perçue
- Pièges courants
- FAQ
1. Mesurer avant d’optimiser
La règle d’or de l’optimisation : ne jamais toucher au code sans avoir mesuré. Beaucoup d’optimisations « intuitives » (OnPush partout, async pipe en boucle) ne servent à rien voire dégradent la performance.
Outils de mesure
- Angular DevTools (extension Chrome/Firefox) : profiler de change detection, hiérarchie de composants, hooks de cycle de vie
- Performance tab du navigateur : flame chart détaillé, identifie les long tasks
- Lighthouse : audit complet performance/accessibilité/SEO
- Web Vitals extension : LCP, INP, CLS en direct
- Bundle analyzer :
ng build --stats-jsonpuiswebpack-bundle-analyzer - Sentry, Datadog RUM : performance en production réelle
Le réflexe pratique
Avant de toucher au code, répondre :
– Qu’est-ce qui est lent exactement ? Le chargement initial ? Une interaction ? Le scroll ?
– À partir de quel seuil mesurable ?
– Quel est l’utilisateur affecté (mobile lent, desktop, bande passante limitée) ?
Optimiser sans cible précise, c’est tirer dans le brouillard.
2. Change detection : modes et stratégies
Angular en 2026 supporte trois modes de change detection.
Default (ZoneJS)
Mode historique. Zone.js intercepte tous les événements asynchrones (clic, timeout, HTTP) et déclenche une vérification de tous les composants. Simple à comprendre, mais peu efficace : chaque clic peut déclencher des vérifications de centaines de composants même non-impactés.
OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
...
})
Le composant ne se re-vérifie que si :
– Une de ses Inputs change (référence)
– Un événement utilisateur s’y produit
– Un Observable consommé via async pipe émet
– On appelle manuellement markForCheck()
Performances grandement améliorées sur des arbres complexes.
Zoneless (Angular 18+)
provideZonelessChangeDetection()
Plus de Zone.js. Le change detection se déclenche uniquement sur les changements de signals. Performances supérieures, bundle plus petit (Zone.js retiré ~50kb), debugging simplifié.
Compatible avec OnPush et standalone components. Recommandé pour tout nouveau projet en 2026.
Recommandation
- Nouveau projet : zoneless + signals + OnPush + standalone
- Projet existant : migrer progressivement, OnPush en premier (composant par composant), puis signals, puis zoneless
- Projet legacy lourd : maintenir le mode default, optimiser les hot paths avec OnPush ciblé
3. OnPush et signals : combo gagnant
@Component({
selector: "app-cart",
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Articles : {{ count() }}</p>
<p>Total : {{ total() }} F CFA</p>
@for (item of items(); track item.id) {
<div>{{ item.nom }}</div>
}
`,
})
export class CartComponent {
private cart = inject(CartService);
items = this.cart.items;
count = computed(() => this.items().length);
total = computed(() => this.items().reduce((s, i) => s + i.prix, 0));
}
OnPush + signals = re-rendus minimaux. Quand items change, seuls les fragments du template qui dépendent de items(), count() ou total() sont actualisés. Le reste du DOM est intact.
Le piège des objets mutés
// Mauvais : mutation, OnPush ne détecte rien
const items = this.items();
items.push(nouveau);
// Pas de notification
// Bon
this.items.update(list => [...list, nouveau]);
Toujours créer une nouvelle référence. Ce principe vaut pour useState en React aussi : c’est un invariant des modèles réactifs modernes.
track dans les @for
<!-- Mauvais : utilise l'index, recrée tout au moindre changement -->
@for (item of items(); track $index) { ... }
<!-- Bon : utilise un identifiant stable -->
@for (item of items(); track item.id) { ... }
Avec un track stable, Angular ne recrée que les nœuds DOM réellement modifiés. Sur des longues listes, c’est crucial.
4. Bundle size et lazy loading
ng build --configuration production --stats-json
npx webpack-bundle-analyzer dist/mon-app/stats.json
Identifier ce qui pèse vraiment. Patterns récurrents :
- moment.js : ~70kb. Remplacer par
date-fnsoudayjs(~10kb). - lodash entier : ~70kb. Importer cible :
import debounce from "lodash-es/debounce". - Angular Material non lazy : kits UI lourds. Importer composant par composant, pas le module entier.
- Locales i18n complètes : importer uniquement les locales nécessaires.
Lazy loading
Vu dans Angular architecture modulaire : chaque feature en route lazy-loaded. Le bundle initial ne contient que la première page.
Imports modulaires Angular Material
// Mauvais
import { MatModule } from "@angular/material";
// Bon
import { MatButtonModule } from "@angular/material/button";
import { MatInputModule } from "@angular/material/input";
Avec les standalone components, importer composant par composant :
import { MatButton } from "@angular/material/button";
@Component({
imports: [MatButton],
...
})
Différer le code non-critique
@defer (on viewport) {
<app-comments [postId]="postId()" />
} @placeholder {
<div>Charger les commentaires...</div>
}
Le bloc @defer charge le composant uniquement quand visible. Réduit drastiquement le code chargé sur le first paint.
5. Optimisation des images
<img ngSrc="hero.webp" width="1200" height="630" priority />
La directive NgOptimizedImage :
– Lazy loading automatique sauf priority
– Dimensions explicites (évite layout shift)
– srcset automatique pour différentes tailles d’écran
– Préconnexion au domaine du loader configuré
Configurer un image loader pour un CDN :
provideImgixLoader("https://example.imgix.net");
// ou Cloudinary, Cloudflare Images, etc.
Pour une seule image au-dessus de la ligne de flottaison : priority accélère le LCP. Pour les autres : laisser le lazy loading faire son travail.
Voir aussi → WordPress vitesse mobile 3G optimiser pour le contexte connexions limitées.
6. Server-Side Rendering et hydration
ng add @angular/ssr
SSR rend le HTML côté serveur avant l’arrivée du JavaScript. Bénéfices :
– LCP plus rapide : le contenu est visible avant l’hydratation
– SEO : Google voit le contenu directement (utile aussi pour réseaux sociaux et OpenGraph)
– First paint perçu plus rapide sur connexions lentes
Hydration
Angular 16+ implémente une hydration efficace : le serveur rend, le client réutilise le DOM existant et ajoute juste l’interactivité. Pas de re-rendu complet côté client.
Streaming SSR
Avec Angular 17+, le SSR peut streamer le HTML : envoyer la coque rapidement puis remplir les parties qui demandent des données plus lentes.
@defer (on idle) {
<app-recommendations />
}
Combiné avec SSR, le premier byte arrive très rapidement, le contenu interactif suit.
Quand SSR vaut le coup
- Sites publics avec SEO important
- Pages où le LCP est critique business
- Pages partagées sur réseaux sociaux (OpenGraph)
Pour des dashboards authentifiés sans contraintes SEO : SSR ajoute de la complexité sans bénéfice clair. SPA suffit.
7. Listes virtualisées
Rendre 10 000 lignes dans le DOM est lent. La virtualisation ne rend que les éléments visibles.
import { CdkVirtualScrollViewport } from "@angular/cdk/scrolling";
@Component({
selector: "app-virtual-list",
standalone: true,
imports: [CdkVirtualScrollViewport, ...],
template: `
<cdk-virtual-scroll-viewport itemSize="50" style="height: 400px">
@for (item of items; track item.id) {
<div class="row">{{ item.nom }}</div>
}
</cdk-virtual-scroll-viewport>
`,
})
À utiliser dès qu’une liste dépasse 100-200 items affichables. Indispensable pour des dashboards avec longues tables.
Pour des grilles ou des layouts plus complexes : @angular/cdk/scrolling propose des viewports personnalisables, ou des libs tierces comme ngx-virtual-scroller.
8. Web Vitals et expérience perçue
Les trois indicateurs de Google :
- LCP (Largest Contentful Paint) : temps avant affichage du plus gros élément. Cible : < 2.5s
- INP (Interaction to Next Paint) : temps de réaction aux interactions. Cible : < 200ms
- CLS (Cumulative Layout Shift) : stabilité visuelle au chargement. Cible : < 0.1
Améliorer LCP
- Image principale en
priorityavecNgOptimizedImage - SSR pour rendre le contenu serveur
- Réduire le bundle initial (lazy loading des features non-critiques)
- CDN pour les assets statiques
- Préconnect aux domaines critiques
Améliorer INP
- OnPush + signals limitent le travail de change detection sur interactions
- Décomposer les long tasks (>50ms) avec
setTimeoutourequestIdleCallback - Web Workers pour les calculs CPU intensifs
- Mode zoneless pour éliminer le surcoût de Zone.js
Améliorer CLS
- Dimensions explicites sur images, iframes, vidéos
- Réserver l’espace pour le contenu chargé en différé (skeletons)
- Éviter d’insérer du contenu au-dessus de ce que l’utilisateur regarde
- Police web avec
font-display: swapet fallback de taille similaire
9. Pièges courants
Subscriptions sans unsubscribe
// Mauvais : memory leak garanti
this.api.getData().subscribe(data => this.data = data);
// Bon : auto-unsubscribe au destroy
this.api.getData().pipe(takeUntilDestroyed()).subscribe(data => this.data = data);
// Encore mieux : signal via toSignal
data = toSignal(this.api.getData(), { initialValue: [] });
Calculs lourds dans le template
<!-- Mauvais : appelé à chaque change detection -->
<div>{{ calculateExpensiveValue(items) }}</div>
<!-- Bon : computed signal -->
<div>{{ expensiveValue() }}</div>
expensiveValue = computed(() => calculateExpensiveValue(this.items()));
Le computed mémorise et recalcule uniquement si les deps changent.
Async pipe en boucle
<!-- Mauvais : nouvelle subscription par item -->
@for (id of ids; track id) {
<div>{{ getUser(id) | async | json }}</div>
}
<!-- Bon : combiner les requêtes en amont -->
users = toSignal(forkJoin(ids.map(id => this.api.getUser(id))));
Pas de track dans @for
Sans track, Angular recrée tous les nœuds DOM à chaque mise à jour de la liste. Sur 100 lignes c’est mineur, sur 10 000 c’est catastrophique.
Voir aussi → Angular signals et RxJS en pratique pour les patterns de subscription.
10. FAQ
Mon Angular est lent, par où commencer ?
- Profiler Angular DevTools sur l’action lente
- Identifier les composants qui se re-rendent inutilement
- Vérifier les
tracksur les@forlongs - Vérifier les subscriptions oubliées
- Mesurer le bundle initial avec bundle-analyzer
Optimisation guidée par mesure, pas par intuition.
OnPush partout ou ciblé ?
Avec signals : OnPush partout est le défaut moderne et bénéfique. Sans signals (legacy) : OnPush ciblé sur les composants qui re-rendent inutilement, identifiés via le profiler.
Zoneless est-il production-ready ?
Oui depuis Angular 18+. À combiner avec composants signal-based. Quelques bibliothèques tierces peuvent encore avoir des problèmes (rares en 2026), à tester avant migration totale.
Mon bundle initial fait 800kb, comment le réduire ?
Étapes : 1) Bundle analyzer pour voir ce qui pèse, 2) Lazy load les routes, 3) Importer les composants Material individuellement, 4) Remplacer moment.js par date-fns, 5) Tree-shake lodash, 6) Différer les composants non-critiques avec @defer.
SSR ralentit-il l’app pour les utilisateurs déjà sur le site ?
Pas si l’hydration est bien faite. Le SSR apporte le first paint plus vite, l’hydration ajoute l’interactivité ensuite. Bien implémenté, c’est mieux que SPA pure pour la plupart des cas publics.
Animations qui rament : que faire ?
Préférer les animations CSS aux animations Angular Animations pour les cas simples (transform, opacity). Pour des animations complexes : Web Animations API natif ou GSAP. Animer transform plutôt que top/left/width (qui forcent un layout).
Comment surveiller la performance en production ?
Sentry Performance, Datadog RUM, ou Google’s web-vitals lib pour envoyer les métriques à un dashboard. Identifier les régressions liées aux déploiements. Lighthouse CI pour ne pas régresser sur chaque PR.
Articles liés (cluster Angular)
- 👉 Angular pour entreprise : guide pratique (pillar)
- 👉 Angular signals et RxJS en pratique
- 👉 Angular architecture modulaire
Article mis à jour le 25 avril 2026. Pour signaler une erreur ou suggérer une amélioration, écrivez-nous.