📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

~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, ChatWidget sont 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 :

  1. Ouvre ton bundle analyzer et identifie les gros composants non critiques
  2. Ajoute @defer (on viewport) sur tout ce qui est under the fold
  3. Ajoute @defer (on idle) sur les widgets périphériques
  4. Ajoute @placeholder avec des skeleton screens cohérents avec ton design
  5. 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

📧 Reste informé(e) !

Reçois les derniers articles et conseils EasyAngularKit directement dans ta boîte mail.

S'inscrire gratuitement

EasyAngularKit

Formation complète pour maîtriser Angular et développer des applications web modernes.

Navigation

Contact

Légal

© 2026 Easy Angular Kit. Tous droits réservés.