Skip to content
Angular ng templates 4 min read

Deferrable Views (@defer)

Deferrable views, introduced as a stable feature in Angular 17, let you lazily load a chunk of a template — and the components it depends on — only when they are actually needed. Instead of shipping every component in the initial JavaScript bundle, @defer splits the marked region into a separate chunk that the browser fetches on demand. This shrinks the initial payload, improves Core Web Vitals like LCP and TBT, and keeps your application snappy without writing a single line of manual lazy-loading plumbing.

How @defer works

@defer is a block in Angular’s built-in control flow syntax, so it works in any standalone component template without imports or directives. Any component, directive, or pipe used only inside a @defer block is automatically code-split into its own lazy chunk. When the configured trigger fires, Angular downloads that chunk, instantiates the content, and swaps it into the DOM.

@defer {
  <app-comments [postId]="postId()" />
}

By default (with no trigger), the block loads on idle — when the browser becomes idle via requestIdleCallback. That single line already removes <app-comments> and its transitive dependencies from the main bundle.

The dependency must be used exclusively inside the @defer block. If the same component is referenced elsewhere in the template (eagerly), it stays in the main bundle and deferring it has no bundle-size benefit.

Placeholder, loading, and error blocks

A bare @defer shows nothing until its content arrives. The companion blocks let you control every phase of the lifecycle.

@defer (on viewport) {
  <app-heavy-chart [data]="metrics()" />
} @placeholder (minimum 500ms) {
  <div class="skeleton">Chart preview</div>
} @loading (after 100ms; minimum 1s) {
  <app-spinner />
} @error {
  <p class="error">Could not load the chart. Please retry.</p>
}
BlockRenders whenNotable parameters
@placeholderBefore loading starts (initial state)minimum — minimum time to show, avoids flicker
@loadingWhile the chunk is being fetchedafter — delay before showing; minimum — minimum visible time
@errorThe lazy chunk fails to loadnone

The after and minimum parameters work together to prevent jarring flashes: after 100ms skips the spinner entirely for fast networks, while minimum 1s guarantees that once shown, it stays long enough to feel intentional.

Triggers

Triggers decide when the deferred content loads. They come in two flavours — on triggers respond to browser/UI events, while when evaluates an arbitrary boolean expression.

TriggerLoads when
on idleThe browser is idle (default)
on viewportThe placeholder (or a referenced element) scrolls into view
on interactionThe user clicks or keys into the placeholder/element
on hoverThe pointer hovers the placeholder/element
on immediateAs soon as the client finishes rendering
on timer(duration)After the given time, e.g. on timer(3s)
when conditionThe boolean expression becomes truthy

You can combine multiple triggers; the block loads when any of them fires:

@defer (on viewport; on timer(5s)) {
  <app-recommendations />
} @placeholder {
  <div class="skeleton-row"></div>
}

Triggering on a referenced element

on viewport, on interaction, and on hover can watch an element outside the @defer block via a template reference variable. This is the common pattern when the placeholder itself is too small to be a meaningful viewport target.

<button #loadTrigger type="button">Show advanced settings</button>

@defer (on interaction(loadTrigger)) {
  <app-advanced-settings />
} @placeholder {
  <p>Advanced settings are available.</p>
}

Conditional loading with when and prefetch

The when trigger ties loading to component state — perfect for signals. You can also prefetch the chunk on one trigger while showing it on another, so the bytes are already cached when the user acts.

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [/* eager components only */],
  templateUrl: './dashboard.component.html',
})
export class DashboardComponent {
  showDetails = signal(false);

  reveal(): void {
    this.showDetails.set(true);
  }
}
<button type="button" (click)="reveal()">View report</button>

@defer (when showDetails(); prefetch on idle) {
  <app-detailed-report />
} @placeholder {
  <p>Report hidden.</p>
} @loading (minimum 300ms) {
  <app-spinner />
}

Here the chunk is prefetched quietly during idle time, but the content only renders once showDetails() flips to true.

Verifying the split

Build the app and inspect the output: the deferred component lands in its own lazy chunk rather than the main bundle.

ng build

Output:

Initial chunk files | Names         |  Raw size
main-A1B2C3D4.js    | main          | 142.07 kB
...
Lazy chunk files    | Names         |  Raw size
chunk-9F8E7D6C.js   | detailed-report | 18.44 kB
chunk-5A4B3C2D.js   | heavy-chart     | 31.10 kB

Best practices

  • Always provide a @placeholder so the layout does not shift when content loads; pair it with minimum to prevent flicker on fast connections.
  • Use prefetch for content the user is very likely to need (e.g. below-the-fold sections) so it feels instant when the real trigger fires.
  • Prefer on viewport and on interaction for genuinely optional content, and reserve on immediate/on idle for content that should always load but need not block the initial render.
  • Ensure deferred components are not imported eagerly elsewhere, or you lose the bundle-splitting benefit entirely.
  • Add an @error block for any chunk whose load can realistically fail, and give users a way to retry.
  • Test deferred views under throttled networks in DevTools to validate your after/minimum timings feel natural.
Last updated June 14, 2026
Was this helpful?