@defer charge une partie du template — et tous les composants, directives et pipes qu’elle contient — dans un chunk JavaScript séparé, téléchargé uniquement quand un trigger se déclenche. C’est le moyen le plus simple de réduire le bundle initial sans restructurer vos routes.
Anatomie d’un bloc
@defer (on viewport) {
<!-- tout ce qui est ici (et ses dépendances) finit dans un chunk séparé -->
<app-comments [postId]="post().id" />
} @placeholder (minimum 300ms) {
<!-- affiché AVANT le déclenchement du trigger ; requis pour on viewport / interaction / hover -->
<div class="comments-skeleton"></div>
} @loading (after 100ms; minimum 500ms) {
<!-- affiché pendant le téléchargement du chunk (after : pas de scintillement si c’est rapide) -->
<app-spinner />
} @error {
<!-- si le téléchargement du chunk échoue -->
<p>Impossible de charger les commentaires.</p>
}| Bloc | Quand il apparaît | Paramètres |
|---|---|---|
@defer | Après le trigger + le téléchargement | triggers, prefetch, hydrate |
@placeholder | Avant le trigger | minimum — évite le scintillement |
@loading | Pendant le téléchargement du chunk | after, minimum |
@error | Si le téléchargement échoue | — |
Triggers
@defer { ... } <!-- par défaut : on idle -->
@defer (on idle) { ... } <!-- requestIdleCallback -->
@defer (on idle(500)) { ... } <!-- v22 : idle, mais au plus tard après 500 ms -->
@defer (on viewport) { ... } <!-- entre à l’écran (IntersectionObserver) -->
@defer (on viewport({ rootMargin: '200px' })) { ... } <!-- v21 : options d’IntersectionObserver -->
@defer (on viewport(trigger)) { ... } <!-- observe un autre élément : <div #trigger> -->
@defer (on interaction) { ... } <!-- click / keydown sur le placeholder -->
@defer (on interaction(btn)) { ... } <!-- click sur un élément précis (#btn) -->
@defer (on hover) { ... } <!-- mouseenter / focusin -->
@defer (on immediate) { ... } <!-- juste après le rendu (mais toujours lazy) -->
@defer (on timer(2s)) { ... } <!-- après un délai -->
@defer (when isOpen()) { ... } <!-- condition (une seule fois : true → chargement) -->
@defer (on viewport; on timer(5s)) { ... } <!-- plusieurs triggers = OU -->Faites défiler dans la zone ci-dessous. Le graphique ne se charge que lorsqu’il entre dans le viewport (avec une marge de 50 px).
Prefetch : télécharger tôt, afficher plus tard
prefetch sépare le moment du téléchargement de celui du rendu. Avec prefetch on idle, le chunk est déjà en cache quand l’utilisateur clique — l’impression est celle d’un chargement instantané.
<!-- Télécharger le chunk tôt, afficher plus tard -->
@defer (on interaction; prefetch on idle) {
<app-rich-editor />
} @placeholder {
<button>Modifier</button>
}
<!-- Typique : prefetch on hover pour les actions « à un clic » -->
@defer (on interaction; prefetch on hover) {
<app-share-dialog />
} @placeholder {
<button>Partager</button>
}Ce qui va dans le chunk différé
// Pour qu’un bloc soit vraiment lazy (chunk séparé), ses dépendances doivent être :
// 1. standalone (par défaut désormais)
// 2. utilisées UNIQUEMENT dans des blocs @defer de ce template
// 3. non référencées autrement dans le même fichier (par ex. dans du code TS, ViewChild, etc.)
@Component({
imports: [CommentsWidget, HeavyChart], // ok : elles n’apparaissent que dans @defer
template: `
@defer (on viewport) { <app-heavy-chart /> }
`,
})
export class Dashboard {
// ✗ cela rendrait HeavyChart « eager » — il est référencé directement dans la classe :
// chart = viewChild(HeavyChart);
}@defer et SSR : Hydration incrémentalev22
Avec le SSR, hydrate on ... rend le HTML sur le serveur, mais ne télécharge et n’active le JS du bloc que lorsque c’est nécessaire. Depuis la v22, l’Hydration incrémentale est le comportement par défaut.
<!-- SSR : Hydration incrémentale — par défaut depuis la v22 (withIncrementalHydration n’est plus nécessaire) -->
@defer (hydrate on viewport) {
<app-product-reviews /> <!-- le HTML arrive déjà rendu du serveur, le JS se charge quand il est visible -->
}
@defer (hydrate on interaction) { <app-filters /> }
@defer (hydrate never) { <app-static-footer /> } <!-- HTML uniquement, sans JS -->Personnaliser « idle » (v22)
// v22 : vous pouvez personnaliser ce que signifie « idle » pour @defer et onIdle (injectAsync)
import { provideIdleServiceWith } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideIdleServiceWith(MyIdleService), // par ex. une implémentation qui attend aussi le chargement des polices
],
};Pour les services lourds, l’équivalent de @defer est injectAsync() — services en Lazy Loading ; pour des pages entières, les routes lazy de Routing moderne et Lazy Loading. Comment @defer se combine avec le SSR est expliqué dans Hydration : event replay et incrémentale.
Quand OUI, quand NON
- Le contenu sous la ligne de flottaison : commentaires, avis, « produits similaires », un footer riche.
- Les composants lourds : graphiques, cartes, éditeurs de texte riche, lecteurs vidéo.
- Une UI masquée jusqu’à une action : dialogues, onglets secondaires, panneaux de paramètres.
- Les fonctionnalités conditionnelles (premium, admin) —
when.
- Le contenu principal « au-dessus de la ligne de flottaison » — cela retarde le LCP et provoque un décalage de mise en page.
- Les petits composants (quelques Ko) — le coût d’une requête n’en vaut pas la peine.
- À la place de
@if— une fois chargé,@deferne se « referme » pas. - Les pages entières — pour cela, vous avez le Lazy Loading au niveau des routes (
loadComponent).
Pièges fréquents
- Un
@placeholdermanquant pouron viewport/interaction/hover: le trigger a besoin d’un élément à observer. - Décalage de mise en page : donnez au placeholder les mêmes dimensions que le contenu final.
whenest « à sens unique » : une fois chargé, le bloc reste même si la condition redevient fausse. Pour masquer, combinez avec@if.- Un composant aussi référencé dans la classe (par ex.
viewChild(HeavyChart)) n’est plus lazy — le compilateur vous avertit. - Prefetch sans trigger principal : depuis la v22, le compilateur vous avertit si vous mettez
prefetchsans trigger de rendu.