~5 min de lecture
@defer : le lazy loading de composants que tu ignores (et qui peut transformer tes Core Web Vitals)
Angular a toujours proposé du lazy loading. Mais jusqu'à la v17, ça se limitait aux routes. Ce que tu charges à l'intérieur d'une page, c'était tout ou rien dès le bundle initial.
C'est là que @defer change tout.
Le problème : tu charges tout, même ce que personne ne voit
Imagine une page produit classique : hero section, carrousel d'images, description, onglet avis clients avec 150 commentaires et un composant de notation complexe, section FAQ avec 30 questions/réponses, widget de chat support rarement utilisé.
En Angular pre-v17, tout ça se retrouve dans le même bundle de page. L'utilisateur qui arrive sur le hero attend que le widget de chat (utilisé par 3% des visiteurs) soit parsé et exécuté.
Résultat mesurable : LCP degradé, TTI élevé, bundle initial inutilement lourd.
// ❌ Avant @defer : tout est importé, tout est chargé immédiatement
@Component({
imports: [
Reviews, // 45 KB avec ses dépendances
Faq, // 22 KB
ChatWidget, // 67 KB (inclut un SDK tiers)
Carousel, // 18 KB
],
template: `
<app-hero />
<app-carousel />
<app-reviews />
<app-faq />
<app-chat-widget />
`
})
export class ProductPage {}
Ces 152 KB de composants se téléchargent et s'exécutent même si l'utilisateur repart après avoir vu le prix.
La solution : @defer et ses triggers
@defer crée un chunk séparé pour les composants concernés. Ce chunk ne se télécharge que quand la condition est remplie.
La syntaxe de base :
@defer {
<app-reviews />
}
C'est suffisant pour que Reviews soit exclu du bundle initial et chargé de façon asynchrone. Mais sans trigger explicite, le chargement arrive rapidement après l'hydratation — pas idéal. C'est là que les triggers entrent en jeu.
Les triggers qui font la différence
on viewport — charge quand l'élément devient visible
Le plus utile. Le composant se charge quand il entre dans le viewport de l'utilisateur.
@defer (on viewport) {
<app-reviews />
} @placeholder {
<div class="reviews-skeleton h-96 animate-pulse bg-gray-100 rounded-lg"></div>
}
L'utilisateur qui ne scrolle jamais ne télécharge jamais Reviews. Ceux qui scrollent le voient apparaître de façon progressive.
on idle — charge quand le navigateur est disponible
Idéal pour les fonctionnalités non critiques. Le navigateur attend un moment creux avant de charger.
@defer (on idle) {
<app-chat-widget />
}
Parfait pour le widget de chat : il se charge en background pendant que l'utilisateur lit la page, sans jamais bloquer le rendu critique.
on interaction — charge au premier clic ou survol
Pour les composants conditionnels qui nécessitent une action utilisateur.
@defer (on interaction) {
<app-rich-text-editor />
} @placeholder {
<button class="btn-secondary">Ajouter un commentaire</button>
}
L'éditeur de texte riche (souvent lourd) ne se charge que si l'utilisateur clique. Le bouton reste affiché en placeholder.
on timer — charge après un délai
@defer (on timer(3s)) {
<app-newsletter-popup />
}
Utile pour les popups ou bannières qui ne doivent pas interférer avec le chargement initial.
when — condition programmatique
La plus flexible. Tu passes un signal ou une expression booléenne.
export class ProductPage {
readonly showReviews = signal(false);
readonly isLoggedIn = inject(AuthService).isLoggedIn;
}
@defer (when showReviews()) {
<app-reviews />
}
@defer (when isLoggedIn()) {
<app-user-dashboard />
}
Combiner des triggers
Tu peux en combiner plusieurs — le composant charge dès que l'une des conditions est vraie :
@defer (on viewport; on timer(5s)) {
<app-related-products />
}
@placeholder, @loading, @error : gérer les états
Un defer sans feedback visuel, c'est un UX cassé. Angular te donne trois blocs pour gérer ça proprement.
@defer (on viewport) {
<app-reviews [productId]="productId()" />
} @placeholder (minimum 200ms) {
<!-- Affiché immédiatement, avant le chargement -->
<div class="h-64 bg-gray-50 rounded animate-pulse"></div>
} @loading (minimum 500ms; after 100ms) {
<!-- Affiché pendant le téléchargement du chunk -->
<div class="flex items-center gap-2">
<span class="loader" />
<span>Chargement des avis…</span>
</div>
} @error {
<!-- Affiché si le chunk échoue à se charger -->
<p class="text-red-500">Impossible de charger les avis. <button (click)="retry()">Réessayer</button></p>
}
Les paramètres minimum et after évitent le flash : minimum 200ms assure que le placeholder reste visible au moins 200ms (évite le flash de contenu), after 100ms retarde l'apparition du spinner de 100ms (évite le spinner pour les chargements rapides).
prefetch : charger en avance, afficher à la demande
C'est la combinaison la plus puissante. Tu peux précharger le chunk selon une condition, mais afficher le composant selon une autre.
@defer (on interaction; prefetch on idle) {
<app-rich-text-editor />
} @placeholder {
<button class="btn-secondary">Rédiger un avis</button>
}
Ici : le chunk se télécharge dès que le navigateur est idle (en background, sans impacter les performances), mais le composant ne s'affiche que si l'utilisateur clique. Résultat : l'interaction est quasi-instantanée, et on n'a pas payé le coût au chargement initial.
@defer (when showDashboard(); prefetch when isLoggedIn()) {
<app-dashboard />
}
Le dashboard se pré-télécharge dès la connexion de l'utilisateur, mais ne s'affiche que quand showDashboard() devient true.
Avant / après : impact concret
Voici le refactor complet de la page produit de tout à l'heure :
// ✅ Après @defer : seuls Hero et Carousel dans le bundle initial
@Component({
imports: [Hero, Carousel],
template: `
<app-hero />
<app-carousel />
@defer (on viewport; prefetch on idle) {
<app-reviews [productId]="productId()" />
} @placeholder {
<div class="h-96 animate-pulse bg-gray-100 rounded-lg"></div>
}
@defer (on viewport) {
<app-faq />
} @placeholder {
<div class="h-64 animate-pulse bg-gray-100 rounded-lg"></div>
}
@defer (on idle) {
<app-chat-widget />
}
`
})
export class ProductPage {
readonly productId = input.required<string>();
}
Ce que tu gagnes :
Reviews,Faq,ChatWidgetsont exclus du bundle initial- Le navigateur charge la page sans les 152 KB superflus
- LCP amélioré : le hero et le carrousel sont visibles sans attendre le reste
- Chaque composant defer crée son propre chunk téléchargé à la demande
Pièges à éviter
1. Ne pas abuser des placeholders complexes
Un placeholder trop lourd annule le bénéfice. Garde-le simple : skeleton screen, div vide, texte statique.
2. @defer n'est pas magique pour les dépendances partagées
Si Reviews et Faq utilisent tous les deux CommonModule ou une lib partagée, Angular ne va pas dupliquer le chunk — mais il ne va pas non plus toujours les isoler parfaitement. Vérifie ton bundle avec nx build --stats-json + webpack-bundle-analyzer ou l'équivalent esbuild.
3. Attention aux composants utilisés hors du @defer
Si tu importes Reviews dans imports: [] et dans un @defer, Angular le met dans le bundle initial. Le defer doit être le seul endroit où le composant est référencé.
4. SSR et hydratation
En mode SSR, le contenu du @defer n'est pas rendu côté serveur par défaut (le placeholder l'est). Si le contenu est critique pour le SEO, reconsidère l'approche ou utilise @defer (on idle) avec un placeholder informatif.
Récap actionnable
| Trigger | Quand l'utiliser |
|---|---|
on viewport |
Sections en bas de page (avis, FAQ, recommandations) |
on idle |
Widgets non critiques (chat, analytics UI, notifications) |
on interaction |
Composants conditionnels (éditeur, modal, dropdown complexe) |
on timer(Xs) |
Popups, bannières différées |
when condition() |
Composants liés à un état applicatif (auth, feature flag) |
prefetch on idle + on interaction |
Meilleur compromis perf/UX pour les actions fréquentes |
Le process :
- Ouvre ton bundle analyzer et identifie les gros composants non critiques
- Ajoute
@defer (on viewport)sur tout ce qui est under the fold - Ajoute
@defer (on idle)sur les widgets périphériques - Ajoute
@placeholderavec des skeleton screens cohérents avec ton design - Mesure avec Lighthouse avant/après
@defer est disponible depuis Angular 17 et ne nécessite aucune configuration supplémentaire. C'est probablement la feature la plus sous-utilisée du framework depuis signals.
📚 Envie de creuser Angular ?
👉🏼 ➡️ Découvre EasyAngularKit