Améliorez les performances Angular avec OnPush, trackBy, lazy loading, defer et une stratégie de rendu propre orientée production.
Mesurer avant d'optimiser
La première règle de la performance Angular est de ne rien optimiser à l'aveugle. Modifier la stratégie de détection de changement ou découper des composants sans avoir identifié le vrai goulot d'étranglement crée de la complexité sans gain mesurable. Les outils de mesure doivent être le point de départ de toute démarche.
Angular DevTools
L'extension Chrome/Firefox Angular DevTools inclut un profiler de détection de changement. Il liste les composants re-rendus à chaque cycle et affiche le temps passé dans chacun. C'est l'outil le plus direct pour identifier un composant qui se re-rend inutilement.
Dans le panel Profiler, lancez un enregistrement, interagissez avec l'interface, puis stoppez. Les barres colorées indiquent quels composants ont été vérifiés. Un composant en orange ou rouge doit attirer l'attention en priorité.
Lighthouse et Core Web Vitals
Lighthouse (disponible dans DevTools → onglet Lighthouse) génère un rapport complet incluant les métriques Core Web Vitals :
- LCP (Largest Contentful Paint) : temps d'affichage du contenu principal — cible < 2,5 s
- CLS (Cumulative Layout Shift) : stabilité visuelle pendant le chargement — cible < 0,1
- INP (Interaction to Next Paint) : réactivité aux interactions utilisateur — cible < 200 ms
Analyser le bundle avec source-map-explorer
Avant d'optimiser le temps de chargement, identifiez ce qui pèse dans le bundle. Générez les stats JSON lors du build puis visualisez-les :
# 1. Générer les stats JSON lors du build de production
ng build --stats-json
# 2. Visualiser le bundle avec source-map-explorer
npx source-map-explorer dist/mon-app/browser/*.js
# Alternative : webpack-bundle-analyzer (interface plus interactive)
npx webpack-bundle-analyzer dist/mon-app/browser/stats.json
Le résultat est une carte interactive montrant quelles dépendances occupent le plus d'espace. Un module inattendu — lodash entier, moment.js, une bibliothèque UI complète — est souvent la cause principale d'un bundle gonflé.
ChangeDetectionStrategy.OnPush
Par défaut, Angular utilise la stratégie Default : à chaque
événement (clic, timer, requête HTTP), il parcourt l'intégralité de l'arbre
de composants pour détecter les changements. Avec OnPush, un
composant n'est vérifié que si l'une des conditions suivantes est remplie :
- Une référence d'
@Input()a changé (nouvel objet ou nouvelle valeur primitive) - Un événement DOM est déclenché depuis ce composant ou l'un de ses enfants
- Un Observable ou Signal lié au template émet une nouvelle valeur
markForCheck()est appelé manuellement depuis le composant
Comparaison Default vs OnPush
| Stratégie | Déclencheur de vérification | Avantage | Risque principal |
|---|---|---|---|
Default |
Tout événement global (zone.js) | Simple, aucune discipline requise | Nombreux re-rendus inutiles |
OnPush |
Changement de référence, signal ou événement local | Rendu minimal, architecture disciplinée | Mutations en place non détectées |
Exemple complet : @Input() immuable et Signal local
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
inject,
signal
} from '@angular/core';
import { CurrencyPipe } from '@angular/common';
@Component({
selector: 'app-product-card',
standalone: true,
imports: [CurrencyPipe],
// OnPush : ce composant est vérifié uniquement sur changement de référence d'input
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="card">
<h3>{{ product.name }}</h3>
<p>{{ product.price | currency:'EUR' }}</p>
<!-- Le signal local déclenche la mise à jour sans markForCheck() -->
<button (click)="toggleFavorite()">
{{ isFavorite() ? '★ Retirer des favoris' : '☆ Ajouter aux favoris' }}
</button>
</div>
`
})
export class ProductCardComponent {
// @Input() immuable : Angular détecte le changement de référence
@Input({ required: true }) product!: { name: string; price: number };
// Signal local : mise à jour granulaire sans cycle de détection global
isFavorite = signal(false);
toggleFavorite(): void {
// update() reçoit la valeur courante et retourne la nouvelle
this.isFavorite.update(current => !current);
}
}
Quand utiliser markForCheck()
Si une donnée externe (WebSocket, push notification, Observable non lié au
template) modifie l'état du composant depuis l'extérieur de la zone Angular,
le cycle de détection ne le saura pas. Injectez ChangeDetectorRef
et appelez markForCheck() pour signaler manuellement que ce
composant doit être re-vérifié au prochain cycle.
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnInit,
inject
} from '@angular/core';
import { WebSocketService } from './websocket.service';
@Component({
selector: 'app-live-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<span>Connexions actives : {{ count }}</span>`
})
export class LiveCounterComponent implements OnInit {
count = 0;
private cdr = inject(ChangeDetectorRef);
private ws = inject(WebSocketService);
ngOnInit(): void {
// Le WebSocket émet en dehors de la zone Angular
this.ws.onConnectionCount$.subscribe(n => {
this.count = n;
// Sans markForCheck(), le template ne se mettrait jamais à jour
this.cdr.markForCheck();
});
}
}
OnPush dès la création
de chaque nouveau composant. C'est beaucoup plus difficile à introduire après
coup sur une base de code avec des mutations d'objets en place.
Signals et computed() pour l'état local
Les Signals (introduits en Angular 16, stables depuis Angular 17) remplacent
avantageusement BehaviorSubject pour gérer l'état local d'un
composant. Ils sont plus lisibles, plus simples à déboguer et s'intègrent
nativement avec OnPush sans aucune configuration supplémentaire.
signal() vs BehaviorSubject
| Critère | BehaviorSubject | Signal |
|---|---|---|
| Lecture de la valeur | subject.getValue() |
mySignal() |
| Mise à jour | subject.next(val) |
mySignal.set(val) |
| Utilisation dans le template | Pipe async obligatoire |
Appel direct mySignal() |
| Intégration OnPush | Besoin de markForCheck() |
Automatique, natif |
| Cleanup | Unsubscribe obligatoire | Aucun cleanup nécessaire |
Pourquoi éviter les méthodes dans les templates
Appeler une méthode directement dans un template Angular
({{ getTotal() }}) force Angular à l'exécuter à chaque cycle de
détection de changement, même si les données n'ont pas changé. Sur une liste
de 100 éléments avec un calcul coûteux, cela représente 100 appels inutiles
par cycle. Utilisez computed() pour mettre le résultat en cache.
Exemple : compteur de panier avec signal() et computed()
import { Component, computed, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
interface CartItem {
name: string;
price: number;
qty: number;
}
@Component({
selector: 'app-cart',
standalone: true,
imports: [CurrencyPipe],
template: `
<h2>Panier ({{ itemCount() }} article{{ itemCount() > 1 ? 's' : '' }})</h2>
<ul>
@for (item of items(); track item.name) {
<li>{{ item.name }} × {{ item.qty }} — {{ item.price * item.qty | currency:'EUR' }}</li>
}
</ul>
<!-- computed() : recalculé uniquement quand items() change -->
<p class="total">Total : <strong>{{ totalPrice() | currency:'EUR' }}</strong></p>
<button (click)="addItem()">Ajouter un article test</button>
`
})
export class CartComponent {
// Signal mutable : source de vérité pour la liste du panier
items = signal<CartItem[]>([
{ name: 'Clavier mécanique', price: 79, qty: 1 },
{ name: 'Souris ergonomique', price: 35, qty: 2 },
]);
// computed() : dérivé de items(), mis en cache, recalcul automatique si items() change
itemCount = computed(() => this.items().reduce((acc, i) => acc + i.qty, 0));
totalPrice = computed(() => this.items().reduce((acc, i) => acc + i.price * i.qty, 0));
addItem(): void {
// update() reçoit la valeur actuelle et retourne la nouvelle valeur immutablement
this.items.update(list => [...list, { name: 'Câble USB-C', price: 12, qty: 1 }]);
}
}
{{ getTotal() }}). Déclarez une propriété
computed() dans le composant et utilisez-la dans le template.
Listes performantes : @for, trackBy et Virtual Scrolling
Les listes sont souvent responsables des problèmes de rendu les plus visibles. Sans identifiant unique, Angular détruit et recrée tous les éléments DOM à chaque mise à jour — même si un seul élément a changé. Avec un identifiant, seuls les nœuds réellement modifiés sont recréés.
@for avec track (Angular 17+)
La nouvelle syntaxe de bloc @for rend le suivi obligatoire. C'est
intentionnel : le compilateur impose que chaque item soit identifiable, ce qui
élimine la classe entière de bugs liée à l'absence de trackBy.
<!-- @for avec track : Angular identifie chaque item par son id -->
@for (user of users(); track user.id) {
<app-user-card [user]="user" />
} @empty {
<p class="text-muted">Aucun utilisateur trouvé.</p>
}
<!-- Si les items n'ont pas d'id unique, utiliser $index en dernier recours -->
@for (item of staticList; track $index) {
<li>{{ item }}</li>
}
*ngFor avec trackBy (syntaxe classique)
Sur les projets antérieurs à Angular 17 ou utilisant encore la syntaxe template
classique, trackBy remplit le même rôle. La fonction de suivi doit
retourner une valeur unique et stable pour chaque item.
// Dans le composant TypeScript : déclarer la fonction de suivi
trackByUserId(index: number, user: { id: number }): number {
// Retourner l'identifiant unique de l'item
// Angular compare cette valeur pour décider si un nœud doit être recréé
return user.id;
}
<!-- Dans le template : passer la référence à trackBy -->
<app-user-card
*ngFor="let user of users; trackBy: trackByUserId"
[user]="user"
/>
Virtual Scrolling avec Angular CDK
Pour les listes de plusieurs centaines ou milliers d'items, le rendu DOM complet est prohibitif : 1 000 items génèrent des milliers de nœuds DOM actifs. Le Virtual Scrolling ne rend que les éléments visibles dans la fenêtre, remplaçant les autres par de l'espace vide.
# Installer Angular CDK si ce n'est pas déjà fait
ng add @angular/cdk
// Dans le composant standalone : importer ScrollingModule
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [ScrollingModule, UserCardComponent],
template: `
<!-- itemSize : hauteur fixe en px de chaque item (obligatoire pour FixedSizeVirtualScrollViewport) -->
<cdk-virtual-scroll-viewport itemSize="72" class="user-list-viewport">
<!-- *cdkVirtualFor remplace *ngFor pour la virtualisation -->
<app-user-card
*cdkVirtualFor="let user of users; trackBy: trackByUserId"
[user]="user"
/>
</cdk-virtual-scroll-viewport>
`,
styles: [`
/* La hauteur du viewport doit être explicite */
.user-list-viewport {
height: 400px;
overflow-y: auto;
/* contain: strict améliore les performances de layout */
contain: strict;
}
`]
})
export class UserListComponent {
users: { id: number; name: string }[] = Array.from({ length: 5000 }, (_, i) => ({
id: i + 1,
name: `Utilisateur ${i + 1}`
}));
trackByUserId(_: number, user: { id: number }): number {
return user.id;
}
}
track ou trackBy suffit largement.
Lazy loading des routes et @defer
Le bundle initial est le principal facteur du temps de chargement perçu.
Toute fonctionnalité non nécessaire au premier écran doit être chargée à la
demande. Angular propose deux mécanismes complémentaires : le lazy loading
des routes pour les modules entiers et les blocs @defer pour les
composants individuels dans un template.
Lazy loading des routes
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
// Chargé immédiatement : fait partie du bundle initial
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent),
},
{
path: 'dashboard',
// Chargé uniquement quand l'utilisateur navigue vers /dashboard
loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent),
},
{
path: 'admin',
// Feature complète (plusieurs routes) chargée en un seul chunk à la demande
loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),
},
];
@defer : chargement différé de composants dans le template
@defer (Angular 17+) permet de retarder le chargement d'un bloc
de composants sans modifier la configuration des routes. Le code JavaScript du
bloc différé est exclu du bundle initial et chargé selon les triggers définis.
<!-- Trigger 1 : on viewport — chargé quand le placeholder entre dans le viewport -->
@defer (on viewport) {
<app-recommendations />
} @placeholder {
<div class="placeholder-box" style="height: 200px;">Chargement des recommandations...</div>
}
<!-- Trigger 2 : on interaction — chargé au premier clic ou focus -->
@defer (on interaction) {
<app-rich-text-editor />
} @placeholder {
<button class="btn btn-secondary">Ouvrir l'éditeur</button>
}
<!-- Trigger 3 : on idle — chargé quand le navigateur est inactif -->
@defer (on idle) {
<app-analytics-widget />
}
<!-- Trigger 4 : when — condition booléenne explicite -->
@defer (when isUserLoggedIn()) {
<app-personalized-feed />
} @loading (after 100ms; minimum 500ms) {
<app-spinner />
} @error {
<p class="text-danger">Impossible de charger le fil personnalisé.</p>
}
Exemple pratique : dashboard avec widgets lourds
<!-- Contenu critique : chargé immédiatement, fait partie du bundle initial -->
<app-kpi-summary [data]="kpis()" />
<app-recent-orders [orders]="recentOrders()" />
<!-- Graphiques lourds : chargés seulement quand visibles dans le viewport -->
<!-- prefetch on idle : télécharge le code pendant que l'utilisateur lit les KPIs -->
@defer (on viewport; prefetch on idle) {
<app-revenue-chart [period]="selectedPeriod()" />
<app-user-funnel-chart />
} @placeholder (minimum 300ms) {
<!-- Skeleton à dimensions fixes pour éviter le layout shift (CLS) -->
<div class="chart-skeleton" style="height: 320px; background: var(--skeleton-bg);"></div>
} @loading (after 100ms; minimum 500ms) {
<app-spinner message="Chargement des graphiques..." />
}
loadChildren) + @defer (on viewport) pour les
composants lourds en bas de page — graphiques, recommandations, widgets sociaux,
éditeurs.
RxJS : éviter les fuites mémoire
Une fuite mémoire RxJS survient quand un composant s'abonne à un Observable et ne résilie pas cet abonnement lors de sa destruction. Le callback continue de s'exécuter sur un composant mort, ce qui provoque des erreurs silencieuses, des comportements imprévisibles et une dégradation progressive des performances.
Anti-pattern : subscribe() manuel sans cleanup
// ❌ À éviter absolument : fuite mémoire garantie si le composant est détruit
@Component({ /* ... */ })
export class BadComponent implements OnInit {
data: string[] = [];
private dataService = inject(DataService);
ngOnInit(): void {
// Ce subscribe() ne sera jamais résilié : le callback vivra indéfiniment
this.dataService.getData().subscribe(result => {
this.data = result;
// Si le composant est détruit, cette ligne s'exécute encore sur un composant mort
});
}
}
Solution 1 : takeUntilDestroyed() (Angular 16+)
takeUntilDestroyed() est l'approche recommandée depuis Angular 16.
L'opérateur RxJS résilie automatiquement l'abonnement quand le contexte
d'injection du composant est détruit. Plus de ngOnDestroy à
implémenter manuellement.
import { Component, DestroyRef, inject, OnInit } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DataService } from './data.service';
@Component({ /* ... */ })
export class GoodComponent implements OnInit {
data: string[] = [];
private dataService = inject(DataService);
// DestroyRef permet d'utiliser takeUntilDestroyed() dans ngOnInit (hors constructeur)
private destroyRef = inject(DestroyRef);
ngOnInit(): void {
this.dataService.getData()
// takeUntilDestroyed() résilie l'abonnement quand le composant est détruit
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(result => {
this.data = result;
});
}
}
Solution 2 : async pipe dans le template
Le pipe async souscrit à l'Observable et le résilie
automatiquement quand le composant est détruit. C'est la solution la plus
concise quand la donnée est uniquement affichée dans le template.
// Composant : exposer l'Observable directement, sans subscribe()
@Component({
selector: 'app-user-list',
standalone: true,
imports: [AsyncPipe],
template: `
<ul>
<!-- async pipe : souscrit et résilie automatiquement -->
@for (user of users$ | async; track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`
})
export class UserListComponent {
// L'Observable est exposé tel quel au template
users$ = inject(UserService).getUsers();
}
Solution 3 : toSignal() — Observable converti en Signal
toSignal() convertit un Observable en Signal. L'abonnement est
résilié automatiquement, le pipe async n'est plus nécessaire et
le Signal s'intègre nativement avec OnPush.
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { UserService } from './user.service';
@Component({
selector: 'app-user-list',
standalone: true,
template: `
<ul>
@for (user of users(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
`
})
export class UserListComponent {
private userService = inject(UserService);
// toSignal() : abonnement géré automatiquement, valeur lisible comme un Signal
users = toSignal(this.userService.getUsers(), { initialValue: [] });
}
Bonnes pratiques RxJS supplémentaires
- Utilisez
switchMappour annuler les requêtes HTTP obsolètes (auto-complétion, recherche live) - Debouncez les événements clavier avec
debounceTime(300)avant d'envoyer une requête - Partagez les flux coûteux avec
shareReplay(1)si plusieurs abonnés consomment la même source - Utilisez
combineLatestplutôt que des abonnements imbriqués pour combiner plusieurs streams
import { toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged, switchMap, of } from 'rxjs';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
// Exemple : champ de recherche avec debounce + switchMap + toSignal
searchResults = toSignal(
this.searchControl.valueChanges.pipe(
// Attendre 300 ms de silence avant d'envoyer la requête
debounceTime(300),
// Ne pas déclencher si la valeur n'a pas changé
distinctUntilChanged(),
// Annuler la requête précédente si une nouvelle valeur arrive (switchMap)
switchMap(query => query
? this.searchService.search(query)
: of([])
),
// Résilier automatiquement quand le composant est détruit
takeUntilDestroyed(),
),
{ initialValue: [] }
);
Optimiser les assets et le bundle
La taille du bundle et le poids des assets impactent directement le temps de chargement initial et les Core Web Vitals. Ces optimisations sont souvent simples à mettre en place et donnent des gains mesurables sans modifier l'architecture des composants.
Images : WebP, dimensions explicites et lazy loading
Les images sont le facteur dominant du LCP et du CLS pour la majorité des applications Angular. Trois attributs font l'essentiel du travail :
<!-- ✅ Image optimisée : WebP, dimensions explicites, lazy loading -->
<img
src="assets/images/hero.webp"
alt="Tableau de bord de l'application"
width="1200"
height="630"
loading="lazy"
decoding="async"
>
<!-- ✅ Image critique (LCP) : chargement prioritaire, pas de lazy -->
<img
src="assets/images/hero-above-fold.webp"
alt="Bannière principale"
width="1200"
height="630"
loading="eager"
fetchpriority="high"
>
<!-- ❌ À éviter : absence de dimensions → layout shift → CLS élevé -->
<img src="assets/images/hero.jpg" alt="...">
ng-container : zéro overhead DOM
ng-container est un conteneur logique invisible : il ne génère
aucun élément dans le DOM réel. Utilisez-le pour grouper des directives
structurelles ou des blocs conditionnels sans polluer le DOM avec des
<div> superflus qui alourdissent le layout et les sélecteurs CSS.
<!-- ❌ Div inutile qui s'ajoute au DOM et peut casser le CSS -->
<div *ngIf="isLoggedIn">
<app-user-menu />
<app-notifications />
</div>
<!-- ✅ ng-container : aucun élément DOM généré, zéro impact sur le layout -->
<ng-container *ngIf="isLoggedIn">
<app-user-menu />
<app-notifications />
</ng-container>
<!-- ✅ Avec la nouvelle syntaxe @if (Angular 17+) : ng-container implicite -->
@if (isLoggedIn) {
<app-user-menu />
<app-notifications />
}
Tree-shaking : imports précis
Le tree-shaking d'Angular supprime le code inutilisé lors du build de
production. Mais des imports trop larges — en particulier import *
— empêchent cet élaguage et font gonfler le bundle inutilement.
// ❌ Import de toute la bibliothèque lodash → tree-shaking impossible → +70 Ko
import * as _ from 'lodash';
const grouped = _.groupBy(items, 'category');
// ✅ Import de la seule fonction nécessaire → seule cette fonction dans le bundle
import groupBy from 'lodash/groupBy';
const grouped = groupBy(items, 'category');
// ✅ Encore mieux : équivalent natif ES2024 (zéro dépendance)
const grouped = Object.groupBy(items, item => item.category);
Services tree-shakables avec providedIn: 'root'
Les services déclarés avec providedIn: 'root' sont
automatiquement tree-shakables : Angular les exclut du bundle si aucun
composant ne les injecte. À l'inverse, un service ajouté dans
providers[] d'un module est toujours inclus, qu'il soit utilisé
ou non.
// ✅ Tree-shakable : inclus dans le bundle seulement si effectivement injecté
@Injectable({ providedIn: 'root' })
export class AnalyticsService {
track(event: string, data?: Record<string, unknown>): void {
// Logique d'envoi des événements analytics
}
}
// ❌ Déclaré dans providers[] d'un module → toujours inclus dans le bundle
@NgModule({
providers: [AnalyticsService] // Présent même si jamais injecté nulle part
})
export class AppModule {}
Checklist performance Angular
Voici un récapitulatif des pratiques couvertes dans cet article, utilisable comme checklist lors d'un audit de performance ou d'une code review.
Mesure et diagnostic
- Angular DevTools installé et profiler utilisé avant toute optimisation
- Lighthouse run effectué, métriques LCP / CLS / INP documentées
- Bundle analysé avec
ng build --stats-json+ source-map-explorer - Dépendances lourdes et inattendues identifiées et traitées
Rendu et état
ChangeDetectionStrategy.OnPushactivé sur tous les composants de présentation- Signals utilisés pour l'état local (préféré à BehaviorSubject)
computed()utilisé pour tout calcul dérivé affiché dans le template- Aucune méthode coûteuse appelée directement dans le template
markForCheck()utilisé uniquement si une mise à jour externe est nécessaire
Listes
trackdéfini sur tous les blocs@fortrackBydéfini sur tous les*ngFor- Virtual Scrolling (
cdk-virtual-scroll-viewport) activé pour les listes de plus de 100 items
Chargement différé
- Toutes les routes non critiques configurées en lazy loading (
loadComponent/loadChildren) @defer (on viewport)utilisé pour les composants lourds en bas de page- Placeholders et skeletons à dimensions fixes pour éviter le layout shift
RxJS
- Aucun
subscribe()manuel sans cleanup explicite takeUntilDestroyed()ou pipeasyncutilisés systématiquementtoSignal()utilisé pour convertir les Observables affichés dans les templatesdebounceTime+switchMapsur toutes les recherches utilisateur
Assets et bundle
- Images converties en WebP, attributs
widthetheighttoujours présents loading="lazy"sur toutes les images hors viewport initialng-containerou@ifutilisés à la place de<div>pour les conditions- Imports précis sur toutes les bibliothèques tierces (pas d'
import *) - Services déclarés avec
providedIn: 'root'pour être tree-shakables
Impact estimé par pratique
| Pratique | Avant | Après | Difficulté |
|---|---|---|---|
| OnPush sur les composants | Tout l'arbre vérifié à chaque événement | Seuls les composants impactés vérifiés | Faible |
| Lazy loading des routes | Bundle initial souvent > 500 Ko | Bundle initial réduit à < 150 Ko | Faible |
| Virtual Scrolling | 5 000+ nœuds DOM pour 1 000 items | ~50 nœuds DOM actifs en permanence | Moyenne |
| Images WebP + lazy | LCP > 4 s, CLS > 0,1 | LCP < 2 s, CLS proche de 0 | Faible |
| @defer (on viewport) | Tous les composants dans le bundle initial | Composants secondaires chargés à la demande | Faible |
| computed() vs méthodes template | Recalcul à chaque cycle de détection | Recalcul uniquement si les dépendances changent | Très faible |
La performance Angular n'est pas un hack ponctuel à appliquer une seule fois. C'est une discipline de rendu, d'architecture et de chargement progressif à maintenir tout au long de la vie du projet. Ces pratiques, appliquées dès le départ, ont un coût d'adoption faible et un impact significatif sur l'expérience utilisateur et les métriques Core Web Vitals.