@defer lädt einen Teil des Templates — und alle darin enthaltenen Komponenten, Direktiven und Pipes — in einen separaten JavaScript-Chunk, der erst heruntergeladen wird, wenn ein Trigger auslöst. So verkleinerst du das initiale Bundle am einfachsten, ohne deine Routen umzubauen.
Aufbau eines Blocks
@defer (on viewport) {
<!-- alles hier (und seine Abhängigkeiten) landet in einem separaten Chunk -->
<app-comments [postId]="post().id" />
} @placeholder (minimum 300ms) {
<!-- wird VOR dem Auslösen des Triggers angezeigt; Pflicht bei on viewport / interaction / hover -->
<div class="comments-skeleton"></div>
} @loading (after 100ms; minimum 500ms) {
<!-- wird angezeigt, während der Chunk lädt (after: kein Flackern bei schnellem Laden) -->
<app-spinner />
} @error {
<!-- wenn der Chunk-Download fehlschlägt -->
<p>Wir konnten die Kommentare nicht laden.</p>
}| Block | Wann er erscheint | Parameter |
|---|---|---|
@defer | Nach Trigger + Download | Trigger, prefetch, hydrate |
@placeholder | Vor dem Trigger | minimum — verhindert Flackern |
@loading | Während der Chunk geladen wird | after, minimum |
@error | Wenn der Download fehlschlägt | — |
Trigger
@defer { ... } <!-- Standard: on idle -->
@defer (on idle) { ... } <!-- requestIdleCallback -->
@defer (on idle(500)) { ... } <!-- v22: idle, spätestens aber nach 500 ms -->
@defer (on viewport) { ... } <!-- tritt in den Bildschirm ein (IntersectionObserver) -->
@defer (on viewport({ rootMargin: '200px' })) { ... } <!-- v21: IntersectionObserver-Optionen -->
@defer (on viewport(trigger)) { ... } <!-- beobachtet ein anderes Element: <div #trigger> -->
@defer (on interaction) { ... } <!-- click / keydown auf dem Placeholder -->
@defer (on interaction(btn)) { ... } <!-- click auf ein bestimmtes Element (#btn) -->
@defer (on hover) { ... } <!-- mouseenter / focusin -->
@defer (on immediate) { ... } <!-- direkt nach dem Rendern (aber trotzdem lazy) -->
@defer (on timer(2s)) { ... } <!-- nach einer Weile -->
@defer (when isOpen()) { ... } <!-- Bedingung (nur einmal: true → laden) -->
@defer (on viewport; on timer(5s)) { ... } <!-- mehrere Trigger = ODER -->Scrolle im Kasten unten. Das Diagramm wird erst geladen, wenn es in den Viewport kommt (mit 50 px Rand).
Prefetch: früh herunterladen, später rendern
prefetch trennt den Zeitpunkt des Downloads vom Zeitpunkt des Renderns. Mit prefetch on idle liegt der Chunk beim Klick bereits im Cache — es fühlt sich an wie sofortiges Laden.
<!-- Chunk früh herunterladen, später rendern -->
@defer (on interaction; prefetch on idle) {
<app-rich-editor />
} @placeholder {
<button>Bearbeiten</button>
}
<!-- Typisch: prefetch on hover für Aktionen „einen Klick entfernt“ -->
@defer (on interaction; prefetch on hover) {
<app-share-dialog />
} @placeholder {
<button>Teilen</button>
}Was in den Lazy-Chunk kommt
// Damit ein Block wirklich lazy ist (eigener Chunk), müssen seine Abhängigkeiten:
// 1. Standalone sein (jetzt der Standard)
// 2. NUR in @defer-Blöcken dieses Templates verwendet werden
// 3. in derselben Datei nicht auf andere Weise eingebunden sein (z. B. im TS-Code referenziert, ViewChild usw.)
@Component({
imports: [CommentsWidget, HeavyChart], // ok: sie erscheinen nur in @defer
template: `
@defer (on viewport) { <app-heavy-chart /> }
`,
})
export class Dashboard {
// ✗ damit wäre HeavyChart „eager“ — es wird direkt in der Klasse referenziert:
// chart = viewChild(HeavyChart);
}@defer und SSR: Incremental Hydrationv22
Bei SSR rendert hydrate on ... das HTML auf dem Server, lädt und aktiviert das JS des Blocks aber erst bei Bedarf. Ab v22 ist Incremental Hydration der Standard.
<!-- SSR: Incremental Hydration — Standard seit v22 (withIncrementalHydration ist nicht mehr nötig) -->
@defer (hydrate on viewport) {
<app-product-reviews /> <!-- das HTML kommt fertig gerendert vom Server, das JS wird geladen, sobald es sichtbar ist -->
}
@defer (hydrate on interaction) { <app-filters /> }
@defer (hydrate never) { <app-static-footer /> } <!-- nur HTML, kein JS -->„idle“ anpassen (v22)
// v22: Du kannst anpassen, was „idle“ für @defer und onIdle (injectAsync) bedeutet
import { provideIdleServiceWith } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideIdleServiceWith(MyIdleService), // z. B. eine Implementierung, die auch auf das Laden der Schriften wartet
],
};Für schwere Services ist das Gegenstück zu @defer injectAsync() — Lazy Services; für ganze Seiten die Lazy-Routen aus Modernes Routing & Lazy Loading. Wie @defer mit SSR zusammenspielt, steht unter Hydration: Event Replay & inkrementell.
Wann ja, wann nein
- Inhalte unterhalb des sichtbaren Bereichs: Kommentare, Bewertungen, „ähnliche Produkte“, ein umfangreicher Footer.
- Schwere Komponenten: Diagramme, Karten, Rich-Text-Editoren, Video-Player.
- UI, die bis zu einer Aktion verborgen ist: Dialoge, sekundäre Tabs, Einstellungsbereiche.
- Bedingte Features (Premium, Admin) —
when.
- Der Hauptinhalt „above the fold“ — er verzögert den LCP und verursacht Layout Shift.
- Kleine Komponenten (wenige KB) — der Overhead eines Requests lohnt sich nicht.
- Als Ersatz für
@if— nach dem Laden „schließt“ sich@defernicht wieder. - Ganze Seiten — dafür gibt es Lazy Loading auf Routenebene (
loadComponent).
Häufige Stolperfallen
- Fehlender
@placeholderbeion viewport/interaction/hover: Der Trigger braucht ein Element, das er beobachten kann. - Layout Shift: Gib dem Placeholder dieselben Abmessungen wie dem finalen Inhalt.
whenist „one-way“: Einmal geladen, bleibt der Block bestehen, auch wenn die Bedingung wieder false wird. Zum Ausblenden kombinierst du ihn mit@if.- Eine Komponente, die auch in der Klasse referenziert wird (z. B.
viewChild(HeavyChart)), ist nicht mehr lazy — der Compiler warnt dich. - Prefetch ohne Haupt-Trigger: Ab v22 warnt der Compiler, wenn du
prefetchohne Render-Trigger angibst.