@defer loads part of the template — and all the components, directives and pipes in it — into a separate JavaScript chunk, downloaded only when a trigger fires. It is the simplest way to shrink the initial bundle without restructuring your routes.
Anatomy of a block
@defer (on viewport) {
<!-- everything here (and its dependencies) ends up in a separate chunk -->
<app-comments [postId]="post().id" />
} @placeholder (minimum 300ms) {
<!-- shown BEFORE the trigger fires; required for on viewport / interaction / hover -->
<div class="comments-skeleton"></div>
} @loading (after 100ms; minimum 500ms) {
<!-- shown while the chunk downloads (after: no flicker on fast loads) -->
<app-spinner />
} @error {
<!-- if the chunk download fails -->
<p>We couldn’t load the comments.</p>
}| Block | When it appears | Parameters |
|---|---|---|
@defer | After the trigger + download | triggers, prefetch, hydrate |
@placeholder | Before the trigger | minimum — avoids flicker |
@loading | While the chunk downloads | after, minimum |
@error | If the download fails | — |
Triggers
@defer { ... } <!-- default: on idle -->
@defer (on idle) { ... } <!-- requestIdleCallback -->
@defer (on idle(500)) { ... } <!-- v22: idle, but at the latest after 500 ms -->
@defer (on viewport) { ... } <!-- enters the screen (IntersectionObserver) -->
@defer (on viewport({ rootMargin: '200px' })) { ... } <!-- v21: IntersectionObserver options -->
@defer (on viewport(trigger)) { ... } <!-- observes another element: <div #trigger> -->
@defer (on interaction) { ... } <!-- click / keydown on the placeholder -->
@defer (on interaction(btn)) { ... } <!-- click on a specific element (#btn) -->
@defer (on hover) { ... } <!-- mouseenter / focusin -->
@defer (on immediate) { ... } <!-- right after rendering (but still lazy) -->
@defer (on timer(2s)) { ... } <!-- after a while -->
@defer (when isOpen()) { ... } <!-- condition (only once: true → load) -->
@defer (on viewport; on timer(5s)) { ... } <!-- several triggers = OR -->Scroll inside the box below. The chart loads only when it enters the viewport (with a 50 px margin).
Prefetch: download early, render later
prefetch separates the moment of download from the moment of rendering. With prefetch on idle, the chunk is already cached when the user clicks — the feeling is instant loading.
<!-- Download the chunk early, render later -->
@defer (on interaction; prefetch on idle) {
<app-rich-editor />
} @placeholder {
<button>Edit</button>
}
<!-- Typical: prefetch on hover for actions “one click away” -->
@defer (on interaction; prefetch on hover) {
<app-share-dialog />
} @placeholder {
<button>Share</button>
}What goes into the lazy chunk
// For a block to be truly lazy (separate chunk), its dependencies must be:
// 1. standalone (default now)
// 2. used ONLY in @defer blocks of this template
// 3. not imported in any other way in the same file (e.g. referenced in TS code, ViewChild etc.)
@Component({
imports: [CommentsWidget, HeavyChart], // ok: they appear only in @defer
template: `
@defer (on viewport) { <app-heavy-chart /> }
`,
})
export class Dashboard {
// ✗ this would make HeavyChart “eager” — it is referenced directly in the class:
// chart = viewChild(HeavyChart);
}@defer and SSR: incremental hydrationv22
With SSR, hydrate on ... renders the HTML on the server, but downloads and activates the block’s JS only when needed. From v22, incremental hydration is the default.
<!-- SSR: incremental hydration — default since v22 (withIncrementalHydration is no longer needed) -->
@defer (hydrate on viewport) {
<app-product-reviews /> <!-- the HTML comes rendered from the server, the JS loads when visible -->
}
@defer (hydrate on interaction) { <app-filters /> }
@defer (hydrate never) { <app-static-footer /> } <!-- HTML only, no JS -->Customising “idle” (v22)
// v22: you can customise what “idle” means for @defer and onIdle (injectAsync)
import { provideIdleServiceWith } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideIdleServiceWith(MyIdleService), // e.g. an implementation that waits for fonts to load too
],
};For heavy services, the equivalent of @defer is injectAsync() — lazy services; for whole pages, the lazy routes in Modern routing & lazy loading. How @defer works with SSR is covered in Hydration: event replay & incremental.
When YES, when NO
- Content below the fold: comments, reviews, “similar products”, a rich footer.
- Heavy components: charts, maps, rich-text editors, video players.
- UI hidden until an action: dialogs, secondary tabs, settings panels.
- Conditional features (premium, admin) —
when.
- The main content “above the fold” — it delays LCP and causes layout shift.
- Small components (a few KB) — the overhead of a request isn’t worth it.
- As a replacement for
@if— after loading,@deferdoesn’t “close” again. - Whole pages — for that you have lazy loading at route level (
loadComponent).
Common pitfalls
- Missing
@placeholderforon viewport/interaction/hover: the trigger needs an element to observe. - Layout shift: give the placeholder the same dimensions as the final content.
whenis “one-way”: once loaded, the block stays even if the condition becomes false again. To hide, combine with@if.- A component referenced in the class too (e.g.
viewChild(HeavyChart)) is no longer lazy — the compiler warns you. - Prefetch without a main trigger: from v22 the compiler warns if you put
prefetchwithout a render trigger.