La première chose que je fais sur n'importe quel codebase Angular est de lancer une analyse du bundle. Le pattern que je cherche est familier : un utilisateur qui ouvre le premier écran télécharge le code de fonctionnalités et d'étapes de parcours qu'il n'atteindra peut-être jamais. Le lazy loading est le remède, et il peut arriver de la façon la plus ennuyeuse qui soit : une route de fonctionnalité à la fois, par des pull requests relues normalement. Pas d'héroïsme, pas de refactoring big-bang, et toujours le gain de performance le moins cher que je connaisse. Les exemples de code ci-dessous utilisent un domaine de demande de prêt uniquement à titre d'illustration ; aucun ne provient du codebase d'un employeur.
Pourquoi la configuration Angular par défaut vous pénalise
Le build par défaut d'Angular met tout dans un seul chunk principal. Chaque composant importé n'importe où dans l'arbre de modules chargés de manière eager finit dans ce chunk. Un module de rapports importé dans app.module.ts ou app.config.ts entraîne chaque bibliothèque de graphiques, chaque composant de tableau et chaque service dont les rapports ont besoin. L'utilisateur qui demande l'écran de connexion paie pour tout.
Le lazy loading déplace le code spécifique aux routes dans des chunks séparés récupérés à la demande. Un utilisateur qui ne navigue jamais vers les rapports ne télécharge jamais le code des rapports. Le First Contentful Paint s'améliore. Le Time to Interactive s'améliore. Les scores Lighthouse s'améliorent. Et l'expérience utilisateur réelle s'améliore sur les connexions qui comptent, qui ne sont jamais les connexions fibre sur lesquelles les développeurs testent.
loadComponent pour les composants de route unique
Avec les composants standalone, c'est trois lignes de code par route.
// app.routes.ts
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./features/dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'applications',
canActivate: [authGuard],
loadComponent: () =>
import('./features/applications/applications.component')
.then(m => m.ApplicationsComponent)
},
{
path: 'reports',
canActivate: [authGuard, roleGuard('analyst')],
loadComponent: () =>
import('./features/reports/reports.component')
.then(m => m.ReportsComponent)
}
];L'import dynamique est la frontière. Le compilateur Angular crée un chunk séparé pour tout ce qui est accessible depuis cet import. Si ReportsComponent importe une bibliothèque de graphiques de 400 Ko et un renderer PDF de 200 Ko, ceux-ci restent hors du bundle principal.
loadChildren pour les arborescences de routes de fonctionnalités
Pour les fonctionnalités avec plusieurs routes imbriquées, loadChildren est le bon choix. Il diffère un fichier de routes entier dans son propre chunk, avec tout ce que ce fichier importe statiquement. Les routes enfants peuvent encore utiliser loadComponent pour découper davantage, comme ci-dessous.
// app.routes.ts
export const routes: Routes = [
{
path: 'lending',
canActivate: [authGuard],
loadChildren: () =>
import('./features/lending/lending.routes')
.then(m => m.LENDING_ROUTES)
}
];
// features/lending/lending.routes.ts
export const LENDING_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./application-list/application-list.component')
.then(m => m.ApplicationListComponent)
},
{
path: ':id',
loadComponent: () =>
import('./application-detail/application-detail.component')
.then(m => m.ApplicationDetailComponent)
},
{
path: ':id/documents',
loadComponent: () =>
import('./document-upload/document-upload.component')
.then(m => m.DocumentUploadComponent)
}
];Préchargement : le lazy sans le délai de navigation
L'inconvénient du lazy loading est que la première navigation vers une route lazy déclenche une requête réseau, ce qui cause un bref délai. Les stratégies de préchargement comblent ce fossé en chargeant les chunks en arrière-plan après le démarrage de l'application, sans bloquer le premier rendu.
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withPreloading(PreloadAllModules))
]
};PreloadAllModules fonctionne pour la plupart des applications. Pour les applications plus grandes où la bande passante compte, un préchargeur sélectif personnalisé charge uniquement les routes haute priorité.
@Injectable({ providedIn: 'root' })
export class PriorityPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
return route.data?.['preload'] === true ? load() : of(null);
}
}
// Mark routes that should preload
{
path: 'applications',
data: { preload: true },
loadChildren: () =>
import('./features/applications/applications.routes')
.then(m => m.APPLICATIONS_ROUTES)
}Comment auditer votre bundle actuel
- Lancez ng build --stats-json pour générer un fichier stats.json (avec le builder application par défaut, c'est un metafile esbuild que l'analyseur de bundle d'esbuild sait lire)
- Buildez avec --source-map, puis lancez npx source-map-explorer dist/your-app/browser/main-*.js pour visualiser le contenu
- Cherchez toute bibliothèque de plus de 50 Ko qui n'est pas nécessaire sur l'écran initial
- Cherchez les composants de fonctionnalités importés directement dans app.config.ts ou les modules chargés en eager
- Identifiez les routes que la majorité des utilisateurs ne visitent jamais. Ce sont les premiers candidats
Les erreurs qui annulent le lazy loading
- Importer un composant lazy directement n'importe où dans l'arbre chargé en eager : l'import annule la frontière lazy
- providedIn: 'any' sur un service utilisé par une route lazy : c'est un comportement documenté plutôt qu'un défaut, et ça piège quand même. Vous obtenez une instance par injecteur lazy, pas un singleton. Utilisez 'root' quand l'état doit être partagé entre fonctionnalités
- Bibliothèques tierces importées dans des modules partagés chargés en eager : déplacez-les vers la fonctionnalité qui en a besoin
- Ne pas utiliser le préchargement et accepter que chaque première navigation vers une route lazy ait un délai visible
Le lazy loading n'est pas une micro-optimisation. C'est souvent le chemin le plus rapide d'une application lente vers une application rapide, mesuré en heures de travail et en secondes d'amélioration.
Lancez l'analyse du bundle d'abord. Les chiffres vous diront exactement par où commencer. Dans chaque application que j'ai auditée, deux ou trois routes représentent l'essentiel du poids inutile du bundle principal. Corrigez celles-ci et mesurez à nouveau. Le ratio effort-impact du lazy loading bat toutes les autres techniques de performance que je connais.
Références
- Angular route loading strategies. https://angular.dev/guide/routing/loading-strategies
- Angular preloading strategies. https://angular.dev/api/router/PreloadingStrategy
- source-map-explorer. https://www.npmjs.com/package/source-map-explorer
- web.dev code splitting guide. https://web.dev/articles/code-splitting-suspense