Maîtrisez les triggers avancés de @defer dans Angular 19+ : on viewport, interaction, hover, timer, idle + prefetch et tests unitaires.
@defer basique vs avancé : la différence
Angular 17 a introduit @defer comme une révolution du chargement différé. Vous connaissez peut-être la forme basique — charger un composant quand il devient visible. Mais @defer va bien plus loin avec une grammaire complète de déclencheurs, de préchargement et de gestion d'états.
Voici la forme basique que beaucoup connaissent :
// @defer basique — charge quand l'élément arrive dans la fenêtre
@defer {
<app-tableau-statistiques></app-tableau-statistiques>
}
// Problème : aucun état de chargement, aucun fallback, déclenchement immédiat
Et voici la forme complète et expressive que nous allons maîtriser :
// @defer complet — toute la grammaire avancée
@defer (on viewport; prefetch on idle) {
<!-- Composant principal — chargé quand visible dans la fenêtre -->
<app-tableau-statistiques [projetId]="projetActif()"></app-tableau-statistiques>
} @placeholder (minimum 200ms) {
<!-- Affiché AVANT le déclenchement — toujours léger -->
<div class="placeholder-container rounded bg-light" style="height: 300px">
<p class="text-center text-muted pt-5">Tableau de bord</p>
</div>
} @loading (minimum 500ms; after 100ms) {
<!-- Affiché PENDANT le téléchargement du chunk -->
<div class="placeholder-glow">
<div class="placeholder col-12 mb-2" style="height: 40px"></div>
<div class="placeholder col-8 mb-2" style="height: 200px"></div>
<div class="placeholder col-4" style="height: 30px"></div>
</div>
} @error {
<!-- Affiché si le chunk échoue à se charger -->
<div class="alert alert-warning d-flex align-items-center gap-2">
<i class="bi bi-wifi-off"></i>
<span>Impossible de charger ce bloc. Vérifiez votre connexion.</span>
</div>
}
@defer est traité par le compilateur Angular. Il découpe automatiquement le composant différé en un chunk séparé. Vous n'écrivez aucun import() manuel.
| Fonctionnalité | @defer basique | @defer avancé |
|---|---|---|
| Déclencheur | Immédiat (on idle par défaut) | 6 triggers + condition signal |
| Préchargement | Non | prefetch on + trigger indépendant |
| États visuels | Aucun | @placeholder + @loading + @error |
| Durée minimale | Non applicable | minimum sur @placeholder et @loading |
| Délai d'affichage | Non applicable | after sur @loading |
Les triggers on : 6 déclencheurs disponibles
Le mot-clé on définit l'événement qui déclenche le chargement du chunk. Angular propose six déclencheurs couvrant tous les scénarios d'interaction.
on viewport — chargement au défilement
Le plus utilisé. Le composant se charge quand le @placeholder entre dans la zone visible de l'écran (détecté via IntersectionObserver).
// Cas d'usage : tableau de bord de projet — sections bas de page chargées au scroll
@Component({
selector: 'app-projet-detail',
standalone: true,
template: `
<!-- Section toujours visible — chargée immédiatement -->
<div class="project-header">
<h1>{{ projet().nom }}</h1>
<p class="text-muted">Chef de projet : {{ projet().responsable }}</p>
</div>
<!-- Timeline des jalons — chargée quand on scrolle jusqu'à elle -->
@defer (on viewport) {
<app-timeline-jalons [jalons]="projet().jalons"></app-timeline-jalons>
} @placeholder {
<div class="bg-light rounded p-4 text-center text-muted" style="height: 250px">
<p class="pt-4">Timeline des jalons</p>
</div>
} @loading (minimum 400ms) {
<div class="placeholder-glow p-2">
@for (i of [1,2,3,4]; track i) {
<div class="d-flex align-items-center gap-3 mb-3">
<div class="placeholder rounded-circle" style="width:40px;height:40px;flex-shrink:0"></div>
<div class="flex-grow-1">
<div class="placeholder col-6 mb-1" style="height:14px"></div>
<div class="placeholder col-4" style="height:12px"></div>
</div>
</div>
}
</div>
}
<!-- Graphique burndown — visible seulement si le projet est en cours -->
@defer (on viewport) {
<app-graphique-burndown [sprints]="projet().sprints"></app-graphique-burndown>
} @placeholder {
<div class="bg-light rounded" style="height: 300px"></div>
}
`
})
export class ProjetDetailComponent {
projet = input.required<Projet>();
}
on interaction — chargement au clic ou focus
// Éditeur de description de tâche — chargé uniquement si l'utilisateur clique
@defer (on interaction) {
<app-editeur-markdown
[contenu]="tache().description"
(changement)="sauvegarderDescription($event)">
</app-editeur-markdown>
} @placeholder {
<!-- Le placeholder lui-même reçoit le clic/focus qui déclenche @defer -->
<div class="border rounded p-3 cursor-pointer text-muted bg-white"
style="min-height: 100px">
@if (tache().description) {
<p>{{ tache().description | slice:0:150 }}...</p>
} @else {
<p>Cliquez pour ajouter une description...</p>
}
</div>
} @loading {
<div class="d-flex align-items-center gap-2 p-3 text-muted">
<div class="spinner-border spinner-border-sm"></div>
<span>Chargement de l'éditeur...</span>
</div>
}
on hover — chargement au survol
// Preview riche d'un membre de l'équipe au survol de son avatar
@defer (on hover) {
<app-carte-membre-detail [membreId]="membre.id"></app-carte-membre-detail>
} @placeholder {
<!-- L'avatar léger est toujours affiché — @defer surveille son survol -->
<div class="avatar-sm" [title]="membre.nom">
<img [src]="membre.avatarMiniature" [alt]="membre.nom"
class="rounded-circle" width="36" height="36">
</div>
}
<!-- Résultat : la carte détaillée (photo haute-res, bio, stats)
ne se charge que si l'utilisateur survole l'avatar -->
on timer(N) — chargement après un délai
// Widget de chat d'assistance — apparu 4 secondes après l'arrivée sur la page
// Laisse l'utilisateur commencer à lire avant d'afficher un widget intrusif
@defer (on timer(4000)) {
<app-widget-support-chat></app-widget-support-chat>
} @placeholder {
<!-- Espace réservé invisible pendant les 4 premières secondes -->
<div></div>
} @loading {
<!-- Apparition discrète en bas à droite -->
<div class="position-fixed bottom-0 end-0 m-3">
<div class="spinner-border text-primary spinner-border-sm"></div>
</div>
}
on idle — chargement pendant l'inactivité
// Suggestions de tâches similaires — chargées quand le navigateur est inactif
// requestIdleCallback est utilisé en interne par Angular
@defer (on idle) {
<app-suggestions-taches [tacheActuelle]="tache()"></app-suggestions-taches>
} @placeholder {
<div class="border-top mt-4 pt-4">
<h6 class="text-muted">Tâches similaires</h6>
<div class="placeholder-glow">
<div class="placeholder col-12 mb-2" style="height: 40px"></div>
<div class="placeholder col-12 mb-2" style="height: 40px"></div>
</div>
</div>
}
on immediate — chargement au plus tôt
// Composant non critique mais prioritaire — chargé dès que possible
// Contrairement au défaut (on idle), ne attend pas l'inactivité
@defer (on immediate) {
<app-bandeau-notifications></app-bandeau-notifications>
}
// Cas d'usage : notifications, alertes système non critiques pour le LCP
// mais à afficher rapidement tout de même
Le trigger when : condition par signal
when est le trigger le plus flexible : il déclenche le chargement dès qu'une expression devient vraie. C'est le seul trigger piloté par votre logique applicative plutôt que par un événement utilisateur.
// Onglet "Rapports" — le composant se charge quand l'onglet est sélectionné
@Component({
selector: 'app-navigation-projet',
standalone: true,
template: `
<ul class="nav nav-tabs mb-3">
<li class="nav-item">
<button class="nav-link" [class.active]="ongletActif() === 'apercu'"
(click)="ongletActif.set('apercu')">Aperçu</button>
</li>
<li class="nav-item">
<button class="nav-link" [class.active]="ongletActif() === 'taches'"
(click)="ongletActif.set('taches')">Tâches</button>
</li>
<li class="nav-item">
<button class="nav-link" [class.active]="ongletActif() === 'rapports'"
(click)="ongletActif.set('rapports')">Rapports</button>
</li>
</ul>
<div class="tab-content">
@if (ongletActif() === 'apercu') {
<app-apercu-projet></app-apercu-projet>
}
@if (ongletActif() === 'taches') {
<app-liste-taches></app-liste-taches>
}
<!-- Rapports : chargés uniquement quand cet onglet est sélectionné -->
<!-- "when" évalue la condition à chaque changement du signal -->
@defer (when ongletActif() === 'rapports') {
<app-rapports-avances [projetId]="projetId()"></app-rapports-avances>
} @placeholder {
<!-- Vide — l'onglet n'est pas actif -->
<div></div>
} @loading (minimum 600ms) {
<div class="text-center py-5">
<div class="spinner-border text-primary mb-3"></div>
<p class="text-muted">Génération des rapports...</p>
</div>
}
</div>
`
})
export class NavigationProjetComponent {
ongletActif = signal<'apercu' | 'taches' | 'rapports'>('apercu');
projetId = input.required<number>();
}
when avec rôle utilisateur
// Panneau d'administration — chargé uniquement si l'utilisateur est admin
// Zéro code admin téléchargé pour les utilisateurs non-admins
@Component({
template: `
<!-- Contenu principal — tout le monde -->
<app-contenu-principal></app-contenu-principal>
<!-- Panneau admin — chargé uniquement pour les admins -->
@defer (when estAdmin()) {
<app-panneau-administration></app-panneau-administration>
}
`
})
export class PageProjetComponent {
private auth = inject(AuthService);
// computed() dérive le statut admin depuis le service d'auth
estAdmin = computed(() => this.auth.utilisateur()?.roles.includes('ADMIN') ?? false);
}
true, le composant se charge et when ne surveille plus la condition. Si la condition repasse à false ensuite (ex: déconnexion admin), le composant reste affiché. Pour un affichage conditionnel dynamique, combinez @defer avec @if.
prefetch : précharger avant d'afficher
prefetch est l'arme secrète de @defer. Il permet de télécharger le chunk en arrière-plan avant même que le déclencheur d'affichage ne soit activé. Résultat : quand l'utilisateur déclenche l'affichage, le composant est déjà là — affichage instantané.
// Scénario : formulaire de création de tâche
// On précharge pendant que le nav est idle, on affiche au clic sur le bouton
@Component({
template: `
<button class="btn btn-primary" #btnNouvelleTache
(click)="afficherFormulaire.set(true)">
+ Nouvelle tâche
</button>
@defer (
on interaction(btnNouvelleTache);
prefetch on idle
) {
<!-- Ce composant est préchargé quand le navigateur est idle -->
<!-- Il s'affiche instantanément au clic sur le bouton -->
<app-formulaire-nouvelle-tache
[projetId]="projetId()"
(cree)="onTacheCree($event)"
(annule)="afficherFormulaire.set(false)">
</app-formulaire-nouvelle-tache>
} @loading (minimum 200ms) {
<div class="text-muted py-3">Chargement du formulaire...</div>
}
`
})
export class EnteteProjetComponent {
projetId = input.required<number>();
afficherFormulaire = signal(false);
private tachesService = inject(TachesService);
onTacheCree(tache: Tache) {
this.tachesService.ajouter(tache);
this.afficherFormulaire.set(false);
}
}
prefetch on hover — précharger au survol
// Menu de navigation — préchargement au survol du lien
// La page sera prête avant même le clic
@Component({
template: `
<nav class="navbar">
<a routerLink="/projets" #lienProjets>Projets</a>
<!-- Précharger le panneau de résumé projets au survol du lien de navigation -->
@defer (
on hover(lienProjets);
prefetch on hover(lienProjets)
) {
<div class="dropdown-preview card shadow-lg">
<app-resume-projets-rapide></app-resume-projets-rapide>
</div>
} @placeholder {
<div></div>
}
</nav>
`
})
export class NavbarComponent {}
Tous les triggers disponibles pour prefetch
| Syntaxe prefetch | Précharge quand... | Idéal combiné avec... |
|---|---|---|
prefetch on idle |
Le navigateur est inactif | on interaction ou on hover |
prefetch on hover |
L'utilisateur survole le placeholder | on interaction (clic) |
prefetch on viewport |
Le placeholder est visible | on interaction |
prefetch on timer(N) |
Après N millisecondes | on interaction |
prefetch when condition |
Une condition signal devient true | Toute combinaison |
@placeholder, @loading et @error
Ces trois blocs gèrent les états visuels du cycle de vie d'un @defer. Chacun a un rôle précis et des options qui permettent d'affiner l'expérience utilisateur.
@placeholder — l'état initial
// @placeholder est affiché AVANT que le trigger ne se déclenche
// Il doit être LÉGER — son code est inclus dans le bundle principal
@defer (on viewport) {
<app-tableau-membres-equipe [equipe]="equipe()"></app-tableau-membres-equipe>
} @placeholder (minimum 300ms) {
<!--
minimum 300ms : le placeholder reste visible au moins 300ms
même si le trigger se déclenche immédiatement
Evite un flash visuel si l'élément est déjà dans le viewport au chargement
-->
<div class="card">
<div class="card-header">Équipe du projet</div>
<div class="card-body placeholder-glow">
@for (i of [1,2,3]; track i) {
<div class="d-flex align-items-center gap-3 mb-3">
<div class="placeholder rounded-circle bg-secondary"
style="width:44px;height:44px;flex-shrink:0"></div>
<div class="flex-grow-1">
<div class="placeholder col-7 mb-1" style="height:14px"></div>
<div class="placeholder col-4" style="height:12px"></div>
</div>
<div class="placeholder col-2" style="height:28px"></div>
</div>
}
</div>
</div>
}
@loading — pendant le téléchargement
// @loading s'affiche pendant le téléchargement du chunk JavaScript
// Deux options : minimum et after
@defer (on interaction) {
<app-editeur-diagramme-gantt [projetId]="projetId()"></app-editeur-diagramme-gantt>
} @placeholder {
<button class="btn btn-outline-primary w-100">
📊 Ouvrir l'éditeur Gantt
</button>
} @loading (after 100ms; minimum 500ms) {
<!--
after 100ms : n'affiche @loading que si le chargement dure plus de 100ms
(évite le flash pour les connexions rapides)
minimum 500ms : si affiché, visible au moins 500ms
(évite un flash trop rapide)
-->
<div class="d-flex flex-column align-items-center py-5 gap-3">
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem"></div>
<div>
<p class="text-center fw-bold mb-1">Chargement de l'éditeur Gantt</p>
<p class="text-center text-muted small">
Première ouverture — environ 2 secondes
</p>
</div>
</div>
}
@error — gestion des échecs de chargement
// @error s'affiche si le téléchargement du chunk échoue
// Causes : perte de réseau, CDN indisponible, fichier 404
@defer (on viewport) {
<app-carte-interactivite-kanban [colonnes]="colonnes()"></app-carte-interactivite-kanban>
} @placeholder {
<div class="kanban-placeholder bg-light rounded" style="height: 400px"></div>
} @loading (minimum 300ms) {
<div class="text-center py-5">
<div class="spinner-border text-primary"></div>
</div>
} @error {
<div class="alert alert-warning">
<div class="d-flex align-items-start gap-3">
<i class="bi bi-exclamation-triangle-fill fs-5 mt-1"></i>
<div>
<strong>Le tableau Kanban n'a pas pu se charger.</strong>
<p class="mb-2 small">Vérifiez votre connexion internet et réessayez.</p>
<button class="btn btn-sm btn-outline-warning"
(click)="$event.target.closest('[defer]')?.retry()">
Réessayer
</button>
</div>
</div>
</div>
}
@placeholder et ses éléments doivent être légers et statiques — ils sont inclus dans le bundle principal. N'y mettez pas de composants complexes. @loading et @error peuvent être plus riches car ils s'affichent seulement à la demande.
Combiner plusieurs triggers
La vraie puissance de @defer vient de la combinaison de triggers. On peut spécifier plusieurs déclencheurs d'affichage (le premier qui se déclenche gagne) et un déclencheur de préchargement distinct.
// Syntaxe de combinaison : séparer les triggers par ";"
// Logique "OU" : le premier trigger qui se déclenche active le chargement
// Exemple 1 : charger si viewport OU si timer dépasse 8 secondes
// (pour les utilisateurs qui scrollent lentement)
@defer (on viewport; on timer(8000); prefetch on idle) {
<app-section-temoignages [projetId]="projetId()"></app-section-temoignages>
} @placeholder {
<div class="bg-light rounded p-4" style="min-height: 200px"></div>
}
// Exemple 2 : combinaison avancée pour un éditeur collaboratif lourd
// - Précharger quand le navigateur est idle (silencieux)
// - Afficher quand l'utilisateur clique sur "Éditer"
@Component({
template: `
<div class="tache-card card mb-3">
<div class="card-body">
<div class="d-flex justify-content-between">
<h6>{{ tache().titre }}</h6>
<button #btnEditer class="btn btn-sm btn-outline-primary">
✏️ Éditer
</button>
</div>
@defer (
on interaction(btnEditer);
prefetch on hover(btnEditer)
) {
<!-- Chargé au clic — mais déjà préchargé si l'user a survolé le bouton -->
<app-editeur-tache-inline
[tache]="tache()"
(sauvegarde)="onSauvegarde($event)"
(annule)="onAnnule()">
</app-editeur-tache-inline>
} @loading (after 80ms; minimum 300ms) {
<div class="p-2 text-muted small">
<span class="spinner-border spinner-border-sm me-2"></span>
Ouverture de l'éditeur...
</div>
} @error {
<div class="alert alert-danger py-2 small">
Impossible d'ouvrir l'éditeur.
</div>
}
</div>
</div>
`
})
export class CarteTacheComponent {
tache = input.required<Tache>();
private tachesService = inject(TachesService);
onSauvegarde(t: Tache) { this.tachesService.mettrAJour(t); }
onAnnule() {}
}
// Exemple 3 : combinaison when + prefetch on idle
// Bandeau de cookies RGPD — visible uniquement si pas encore accepté
// et préchargé dès que le navigateur est inactif
@defer (
when !consentementDonne();
prefetch on idle
) {
<app-bandeau-cookies-rgpd
(accepte)="enregistrerConsentement(true)"
(refuse)="enregistrerConsentement(false)">
</app-bandeau-cookies-rgpd>
}
Cas d'usage réels en production
Voici une application de gestion de projet complète qui utilise @defer de façon stratégique sur sa page principale. Chaque décision est justifiée par un objectif de performance.
// page-tableau-de-bord.component.ts — orchestration complète de @defer
@Component({
selector: 'app-tableau-de-bord',
standalone: true,
template: `
<!-- ZONE 1 : Hero section — chargée immédiatement (LCP critique) -->
<div class="dashboard-hero">
<h1>Bonjour, {{ utilisateur().prenom }} 👋</h1>
<div class="row g-3">
<!-- 4 métriques clés — toujours visibles, dans le bundle principal -->
@for (metric of metriquesRapides(); track metric.label) {
<div class="col-md-3">
<div class="card text-center p-3">
<span class="display-6 fw-bold" [class]="metric.couleur">{{ metric.valeur }}</span>
<small class="text-muted">{{ metric.label }}</small>
</div>
</div>
}
</div>
</div>
<div class="row mt-4 g-4">
<!-- ZONE 2 : Liste des projets actifs — chargée immédiatement (fold supérieur) -->
<div class="col-lg-8">
<app-liste-projets-actifs [projets]="projetsActifs()"></app-liste-projets-actifs>
</div>
<!-- ZONE 3 : Activité récente — viewport + prefetch idle -->
<!-- Visible dans le fold sur grand écran, mais lazy sur mobile -->
<div class="col-lg-4">
@defer (on viewport; prefetch on idle) {
<app-flux-activite-recente [equipeId]="equipeId()"></app-flux-activite-recente>
} @placeholder (minimum 200ms) {
<div class="card" style="height: 400px">
<div class="card-header">Activité récente</div>
<div class="card-body placeholder-glow">
@for (i of [1,2,3,4,5]; track i) {
<div class="placeholder col-12 mb-3" style="height: 60px"></div>
}
</div>
</div>
}
</div>
</div>
<!-- ZONE 4 : Graphiques analytiques — en dessous du fold, lourds -->
<div class="row mt-4 g-4">
<div class="col-md-6">
@defer (on viewport; prefetch on idle) {
<app-graphique-progression-sprints [equipeId]="equipeId()"></app-graphique-progression-sprints>
} @placeholder {
<div class="card bg-light" style="height: 280px"></div>
} @loading (after 100ms; minimum 400ms) {
<div class="card" style="height: 280px">
<div class="card-body d-flex align-items-center justify-content-center">
<div class="spinner-border text-primary"></div>
</div>
</div>
}
</div>
<div class="col-md-6">
@defer (on viewport; prefetch on idle) {
<app-graphique-charge-equipe [equipeId]="equipeId()"></app-graphique-charge-equipe>
} @placeholder {
<div class="card bg-light" style="height: 280px"></div>
} @loading (after 100ms; minimum 400ms) {
<div class="card" style="height: 280px">
<div class="card-body d-flex align-items-center justify-content-center">
<div class="spinner-border text-primary"></div>
</div>
</div>
}
</div>
</div>
<!-- ZONE 5 : Widget de support — timer 5s -->
@defer (on timer(5000); prefetch on idle) {
<app-widget-support></app-widget-support>
}
`
})
export class TableauDeBordComponent {
private auth = inject(AuthService);
private projetsService = inject(ProjetsService);
utilisateur = this.auth.utilisateur;
equipeId = computed(() => this.utilisateur()?.equipeId ?? 0);
projetsActifs = this.projetsService.actifs;
metriquesRapides = computed(() => [
{ label: 'Tâches actives', valeur: 12, couleur: 'text-primary' },
{ label: 'Sprints en cours', valeur: 3, couleur: 'text-success' },
{ label: 'PR en attente', valeur: 7, couleur: 'text-warning' },
{ label: 'Bugs ouverts', valeur: 4, couleur: 'text-danger' },
]);
}
Tester @defer et bonnes pratiques
Tester les blocs @defer dans les tests unitaires
// Angular fournit des helpers pour tester chaque état de @defer
import { TestBed, ComponentFixture } from '@angular/core/testing';
import { DeferBlockState } from '@angular/core/testing';
describe('TableauDeBordComponent', () => {
let fixture: ComponentFixture<TableauDeBordComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TableauDeBordComponent]
}).compileComponents();
fixture = TestBed.createComponent(TableauDeBordComponent);
});
it('devrait afficher le placeholder par défaut', async () => {
fixture.detectChanges();
// Les blocs @defer sont en état "placeholder" par défaut dans les tests
const placeholder = fixture.nativeElement.querySelector('.kanban-placeholder');
expect(placeholder).toBeTruthy();
});
it('devrait afficher le composant une fois chargé', async () => {
fixture.detectChanges();
// Récupérer le premier bloc @defer (index 0)
const deferBlocks = await fixture.getDeferBlocks();
// Forcer le rendu de l'état principal (comme si le trigger s'était déclenché)
await deferBlocks[0].render(DeferBlockState.Complete);
fixture.detectChanges();
// Vérifier que le vrai composant est maintenant rendu
const composant = fixture.nativeElement.querySelector('app-carte-interactivite-kanban');
expect(composant).toBeTruthy();
});
it('devrait afficher le spinner pendant le chargement', async () => {
fixture.detectChanges();
const deferBlocks = await fixture.getDeferBlocks();
// Forcer l'état "loading"
await deferBlocks[0].render(DeferBlockState.Loading);
fixture.detectChanges();
const spinner = fixture.nativeElement.querySelector('.spinner-border');
expect(spinner).toBeTruthy();
});
it('devrait afficher un message d\'erreur si le chunk échoue', async () => {
fixture.detectChanges();
const deferBlocks = await fixture.getDeferBlocks();
// Simuler un échec de chargement
await deferBlocks[0].render(DeferBlockState.Error);
fixture.detectChanges();
const erreur = fixture.nativeElement.querySelector('.alert-warning');
expect(erreur).toBeTruthy();
});
});
Checklist @defer avancé
- Chaque
@defera un@placeholder— jamais de blanc avant le chargement - Le contenu de
@placeholderest léger (pas de composants complexes) @loadingutiliseafter 100mspour éviter le flash sur connexions rapides@loadingutiliseminimum 300ms+pour éviter le flash sur connexions très rapides- Tous les blocs
@deferde composants critiques ont un@error - Les composants dans
@deferne sont pas listés dansimports[]du composant parent prefetch on idleajouté dès qu'un déclencheur manuel est utilisé (interaction, hover)- Les 4 états DeferBlockState sont testés pour chaque bloc @defer critique
- Les composants lourds (+30 Ko) sont systématiquement dans un
@defer - source-map-explorer vérifié après l'ajout de @defer pour confirmer la séparation en chunk
@defer est l'outil le plus puissant d'Angular pour le chargement progressif au sein d'une page. La combinaison on viewport + prefetch on idle couvre 80% des cas : les composants bas de page sont préchargés discrètement et s'affichent instantanément quand l'utilisateur scrolle. Ajoutez after 100ms; minimum 300ms sur @loading pour une expérience fluide quelle que soit la vitesse de connexion.