@defer încarcă o parte din template — și toate componentele, directivele și pipe-urile din ea — într-un chunk JavaScript separat, descărcat doar când un trigger se declanșează. E cel mai simplu mod de a micșora bundle-ul inițial fără să restructurezi rutele.
Anatomia unui bloc
@defer (on viewport) {
<!-- tot ce e aici (și dependențele lui) ajunge într-un chunk separat -->
<app-comments [postId]="post().id" />
} @placeholder (minimum 300ms) {
<!-- afișat ÎNAINTE de declanșare; obligatoriu pentru on viewport / interaction / hover -->
<div class="comments-skeleton"></div>
} @loading (after 100ms; minimum 500ms) {
<!-- afișat cât se descarcă chunk-ul (after: nu clipește la încărcări rapide) -->
<app-spinner />
} @error {
<!-- dacă descărcarea chunk-ului eșuează -->
<p>Nu am putut încărca comentariile.</p>
}| Bloc | Când apare | Parametri |
|---|---|---|
@defer | După trigger + descărcare | trigger-e, prefetch, hydrate |
@placeholder | Înainte de trigger | minimum — evită clipirea |
@loading | Cât se descarcă chunk-ul | after, minimum |
@error | Dacă descărcarea eșuează | — |
Trigger-e
@defer { ... } <!-- implicit: on idle -->
@defer (on idle) { ... } <!-- requestIdleCallback -->
@defer (on idle(500)) { ... } <!-- v22: idle, dar cel târziu după 500 ms -->
@defer (on viewport) { ... } <!-- intră în ecran (IntersectionObserver) -->
@defer (on viewport({ rootMargin: '200px' })) { ... } <!-- v21: opțiuni IntersectionObserver -->
@defer (on viewport(trigger)) { ... } <!-- observă alt element: <div #trigger> -->
@defer (on interaction) { ... } <!-- click / keydown pe placeholder -->
@defer (on interaction(btn)) { ... } <!-- click pe un element anume (#btn) -->
@defer (on hover) { ... } <!-- mouseenter / focusin -->
@defer (on immediate) { ... } <!-- imediat după randare (dar tot lazy) -->
@defer (on timer(2s)) { ... } <!-- după un timp -->
@defer (when isOpen()) { ... } <!-- condiție (o singură dată: true → încarcă) -->
@defer (on viewport; on timer(5s)) { ... } <!-- mai multe triggere = OR -->Derulează în caseta de mai jos. Graficul se încarcă abia când intră în viewport (cu o marjă de 50 px).
Prefetch: descarcă devreme, randează mai târziu
prefetch separă momentul descărcării de momentul randării. Cu prefetch on idle, chunk-ul e deja în cache când utilizatorul dă click — senzația e de încărcare instantă.
<!-- Descarcă chunk-ul devreme, randează mai târziu -->
@defer (on interaction; prefetch on idle) {
<app-rich-editor />
} @placeholder {
<button>Editează</button>
}
<!-- Tipic: prefetch on hover pentru acțiuni „la un click distanță” -->
@defer (on interaction; prefetch on hover) {
<app-share-dialog />
} @placeholder {
<button>Distribuie</button>
}Ce intră în chunk-ul lazy
// Ca un bloc să fie cu adevărat lazy (chunk separat), dependențele lui trebuie să fie:
// 1. standalone (implicit acum)
// 2. folosite DOAR în blocuri @defer din acest template
// 3. neimportate în alt mod în același fișier (ex. referite în cod TS, ViewChild etc.)
@Component({
imports: [CommentsWidget, HeavyChart], // ok: apar doar în @defer
template: `
@defer (on viewport) { <app-heavy-chart /> }
`,
})
export class Dashboard {
// ✗ asta ar face HeavyChart „eager” — e referit direct în clasă:
// chart = viewChild(HeavyChart);
}@defer și SSR: hidratare incrementalăv22
Cu SSR, hydrate on ... randează HTML-ul pe server, dar descarcă și activează JS-ul blocului doar când e nevoie. Din v22, hidratarea incrementală e implicită.
<!-- SSR: hidratare incrementală — implicită din v22 (withIncrementalHydration nu mai e necesar) -->
@defer (hydrate on viewport) {
<app-product-reviews /> <!-- HTML-ul vine randat de la server, JS-ul se încarcă la vedere -->
}
@defer (hydrate on interaction) { <app-filters /> }
@defer (hydrate never) { <app-static-footer /> } <!-- doar HTML, fără JS -->Personalizarea lui „idle” (v22)
// v22: poți personaliza ce înseamnă „idle” pentru @defer și onIdle (injectAsync)
import { provideIdleServiceWith } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideIdleServiceWith(MyIdleService), // ex. o implementare care așteaptă și încărcarea fonturilor
],
};Pentru servicii grele, echivalentul lui @defer e injectAsync() — servicii lazy; pentru pagini întregi, rutele lazy din Routing modern & lazy loading. Cum se combină @defer cu SSR găsești la Hidratare: event replay & incremental.
Când DA, când NU
- Conținut sub fold: comentarii, recenzii, „produse similare”, footer bogat.
- Componente grele: grafice, hărți, editoare rich-text, video player.
- UI ascuns până la o acțiune: dialoguri, tab-uri secundare, panouri de setări.
- Funcționalități condiționate (premium, admin) —
when.
- Conținutul principal „above the fold” — întârzie LCP și produce layout shift.
- Componente mici (câțiva KB) — overhead-ul unui request nu merită.
- Ca înlocuitor pentru
@if— după încărcare,@defernu „se închide” la loc. - Pagini întregi — pentru asta ai lazy loading la nivel de rută (
loadComponent).
Capcane frecvente
- Lipsește
@placeholderpentruon viewport/interaction/hover: trigger-ul are nevoie de un element pe care să-l observe. - Layout shift: dă placeholder-ului aceleași dimensiuni ca conținutului final.
whene „one-way”: odată încărcat, blocul rămâne chiar dacă condiția redevine false. Pentru ascundere combină cu@if.- Componenta referită și în clasă (ex.
viewChild(HeavyChart)) nu mai e lazy — compilatorul te avertizează. - Prefetch fără trigger principal: din v22 compilatorul avertizează dacă pui
prefetchfără trigger de randare.