Dans Angular moderne, les bonnes performances viennent surtout des valeurs par défaut : OnPush, Signals, zoneless, routes lazy, @defer. La checklist ci-dessous est ce que vous vérifiez quand quelque chose est lent, dans l’ordre où il vaut la peine de le faire. Si l’application ralentit à mesure que l’onglet reste ouvert, ce n’est généralement pas le rendu, mais une fuite de mémoire.
Checklist
RENDU
[ ] Composants OnPush (par défaut en v22) + état dans des Signals
[ ] @for avec track sur un id stable (pas l’identité sur des données rechargées)
[ ] Calculs dans computed() / une pipe pure, pas dans des méthodes appelées depuis le template
[ ] Très grandes listes → virtual scroll (CDK ScrollingModule) ou pagination
CHARGEMENT
[ ] Toutes les pages en lazy (loadComponent / loadChildren)
[ ] Composants lourds sous la ligne de flottaison → @defer (on viewport / interaction)
[ ] Services lourds, rarement utilisés → injectAsync() (v22)
[ ] Images avec NgOptimizedImage (ngSrc, width/height, priority sur le LCP)
[ ] Budgets de bundle dans angular.json ; vérifiez ce qu’il contient avec source-map-explorer
SSR (le cas échéant)
[ ] Hydration incrémentale (par défaut en v22) + @defer (hydrate on ...)
[ ] Event replay pour les clics faits pendant l’hydration
[ ] TransferCache pour les requêtes faites sur le serveurtrack compte plus que vous ne le pensez
Quand des données rechargées depuis le serveur arrivent sous forme de nouveaux objets, track row (identité) fait considérer chaque ligne comme « nouvelle » à Angular, qui détruit et recrée tout le DOM. Avec track row.id, il réutilise les lignes existantes et ne met à jour que ce qui a changé.
Bonus : en mode dev, Angular détecte lui-même le problème et affiche l’avertissement NG0956 (« track by identity caused re-creation of the entire collection »).
Images
<!-- NgOptimizedImage: lazy par défaut, srcset automatique, avertissements si les dimensions manquent -->
<img ngSrc="/assets/hero.jpg" width="1200" height="600" priority /> <!-- l’image du LCP -->
<img ngSrc="/assets/avatar.jpg" width="48" height="48" /> <!-- lazy -->
<img ngSrc="product.jpg" fill /> <!-- remplit le conteneur -->Très grandes listes
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
imports: [ScrollingModule],
template: `
<cdk-virtual-scroll-viewport itemSize="48" class="viewport">
<div *cdkVirtualFor="let row of rows(); trackBy: trackById" class="row">{{ row.name }}</div>
</cdk-virtual-scroll-viewport>
`,
styles: `.viewport { height: 480px; }`,
})
export class BigList {
readonly rows = input.required<Row[]>();
protected trackById = (_: number, r: Row) => r.id;
}Bundle
// angular.json → configurations.production.budgets
"budgets": [
{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
]SSR et Hydration
// app.config.server / app.config.ts avec SSR
provideClientHydration(
withEventReplay(), // les clics faits pendant l’hydration ne sont pas perdus
// withIncrementalHydration() — par défaut depuis la v22
);Pièges fréquents
- Optimiser sans mesurer : ouvrez le Profiler dans Angular DevTools avant de modifier le moindre code.
- Méthodes coûteuses dans le template (
sortedItems()en tant que méthode) → elles s’exécutent à chaque rendu. Transformez-les encomputed. - Grosses bibliothèques importées « pour une seule fonction » (tout lodash, moment.js) → imports ciblés ou
injectAsync. @defersur le contenu principal → un LCP dégradé. Ne différez que ce qui est sous la ligne de flottaison.