Angular 74 🅰️ Deferrable Views with @defer
In the previous chapters, every component you rendered was loaded and initialized as part of the main bundle. Whether the user scrolled to it, clicked it, or never saw it at all, the JavaScript for that component was downloaded, parsed, and executed. @defer changes that contract. It is a declarative block that tells Angular: do not load this code now; load it when I say so .
Deferrable views, introduced in Angular v17, are the framework’s built-in solution for lazy loading at the template level. Instead of managing dynamic imports, setting up Intersection Observers, or attaching event listeners manually, you wrap a section of your template in a @defer block and declare when it should load. Angular’s compiler transforms the static imports inside the block into dynamic imports, the bundler splits them into separate chunks, and the runtime fetches them only when a trigger fires . The result is a smaller initial bundle, faster time-to-interactive, and a template that expresses lazy-loading intent without imperative code.
Key point: Only the content inside the @defer block is lazy-loaded. The @placeholder, @loading, and @error blocks are eagerly loaded — their dependencies ship in the main bundle. This means you should keep placeholder content lightweight, and never put heavy components in a placeholder .
Why @defer exists
Before @defer, lazy-loading a component in an Angular template required imperative code. You had to use dynamic import(), manage the loading state manually, and wire up triggers yourself.
The boilerplate problem. A typical lazy-load implementation before v17 looked like this: a @if block checking a boolean, a ngOnInit that calls import('./heavy.component'), a setTimeout or IntersectionObserver to decide when to trigger the import, and error handling for the case where the import fails. That’s dozens of lines of imperative code for a feature that should be declarative .
The bundle problem. Route-level lazy loading handles the large chunks, but it doesn’t help with components within a route. A dashboard might have a heavy chart component below the fold that the user never scrolls to. Without @defer, that chart’s code ships in the route’s bundle and downloads even if it’s never seen .
The intent problem. Imperative lazy-load code obscures the intent. A reader looking at @if (isVisible) doesn’t know that the block is lazy-loaded, or what triggers it, or what placeholder is shown while loading. @defer (on viewport) announces all of that in one line .
The trade-off. @defer is one-way. Once a defer block is triggered and its content rendered, it does not revert to the placeholder if the condition becomes false. The swap is a one-time operation. If you need content that toggles based on a boolean, wrap the @defer in an @if block, or use @if alone without deferral .
a. The @defer Block and Its Sub-blocks
A @defer block is the primary container. Its content is lazy-loaded. By default, it is triggered when the browser reaches an idle state, using the requestIdleCallback API .
The block accepts optional sub-blocks that handle different stages of the loading lifecycle. The @placeholder block declares what to show before the defer block is triggered. It is optional, but the Angular team recommends always including one for the best user experience. The placeholder’s dependencies are eagerly loaded, so keep it lightweight — a skeleton, a static image, or a simple loading message .
@defer {
<large-component />
} @placeholder {
<p>Placeholder content</p>
}
The @placeholder block accepts an optional minimum parameter to prevent flickering. If the deferred content loads faster than the minimum duration, the placeholder stays visible until the timer expires, avoiding a jarring flash .
@defer {
<large-component />
} @placeholder (minimum 500ms) {
<p>Placeholder content</p>
}
The @loading block is shown while the deferred dependencies are being fetched. It replaces the @placeholder once loading begins. It accepts two optional parameters: minimum (the minimum time to show the loading state) and after (the time to wait after loading begins before showing the loading template). Both timers begin immediately after the loading is triggered .
@defer {
<large-component />
} @loading (after 100ms; minimum 1s) {
<img alt="loading..." src="loading.gif" />
}
The @error block is displayed if the deferred loading fails. This is your opportunity to show a retry button or a friendly error message. Like the other sub-blocks, its dependencies are eagerly loaded .
@defer {
<large-component />
} @error {
<p>Failed to load large component.</p>
<button (click)="retry()">Retry</button>
}
The @placeholder and @loading blocks are distinct. The placeholder shows before the trigger fires. The loading block shows during the fetch. A component might go from placeholder to loading to content, or from placeholder directly to content if the fetch is fast enough that the loading block is skipped by its after parameter .
b. Triggers: Controlling When Content Loads
Triggers determine when the defer block fires. There are two types: on triggers, which use built-in conditions, and when triggers, which use custom expressions .
The on triggers are:
idle is the default. It loads when the browser reaches an idle state via requestIdleCallback. This is the safest default because it doesn’t block anything the user is actively doing .
viewport loads when the specified content enters the viewport, using the Intersection Observer API. By default, the @placeholder acts as the observed element, and it must have a single root element. Alternatively, you can reference another element in the template with a template reference variable: @defer (on viewport(greeting)) watches the element with #greeting .
interaction loads when the user interacts with the specified element through click or keydown events. The placeholder is the default interaction target. You can also pass a template reference variable .
hover loads when the mouse hovers over the specified area via mouseover and focusin events. This is useful for preloading content the user is about to interact with — a dropdown menu, a tooltip, or a preview panel .
immediate loads immediately after all non-deferred content has finished rendering. This is useful for content that should load as soon as possible but doesn’t need to block the initial render .
timer loads after a specified duration, in milliseconds or seconds: @defer (on timer(5s)) .
Multiple triggers can be combined with semicolons and are evaluated as OR conditions: @defer (on viewport; on timer(5s)) fires when either the element enters the viewport or five seconds pass, whichever comes first .
The when trigger accepts a custom boolean expression. When the expression becomes truthy, the defer block fires. Like the on triggers, the swap is one-time — if the condition becomes false again, the content does not revert to the placeholder .
@defer (when isReady) {
<large-component />
} @placeholder {
<p>Loading...</p>
}
A common mistake is trying to use the as alias syntax with when: @defer (when items$ | async; as items). This is not supported. Only @if blocks allow the as alias. If you need the resolved value in the deferred block, move the async pipe and alias to an @if block that wraps the @defer .
c. Prefetching and Advanced Patterns
Prefetching loads the JavaScript for a deferred block before the block is actually rendered. This means the code is already in the browser’s cache when the trigger fires, eliminating the network round-trip from the critical path .
@defer (on viewport; prefetch on idle) {
<large-component />
}
In this example, the block renders when it enters the viewport, but the JavaScript is fetched when the browser is idle. By the time the user scrolls to the component, the code is ready and the render is instant .
Prefetch triggers use the same conditions as render triggers: prefetch on idle, prefetch on viewport, prefetch on hover, prefetch on timer(2s), prefetch on immediate, and prefetch when condition .
The interaction between prefetch and render is important. Prefetching does not affect when the block renders. It only affects when the code is downloaded. The render trigger still controls visibility. If the prefetch finishes before the render trigger fires, the content appears instantly. If the render trigger fires first, the user sees the placeholder or loading block while the fetch completes .
A powerful pattern for below-the-fold content is @defer (on viewport; prefetch on idle). The user never sees a loading state because the code is already fetched by the time they scroll to it .
There is one important limitation with viewport and interaction triggers: they require either a @placeholder block with a single root element or an explicit template reference variable. If neither is present, the trigger has nothing to observe .
Complete Example Session
This session builds a comments section that loads on viewport, with placeholder, loading, and error states, then adds prefetching and a custom when trigger.
// ============================================
// PART 1: THE BASIC DEFER BLOCK
// ============================================
@Component({
selector: 'app-post',
template: `
<article>
<h1>Post Title</h1>
<p>Post content goes here.</p>
</article>
@defer {
<app-comments [postId]="postId" />
}
`,
})
export class PostComponent {
postId = 1;
}
// ============================================
// PART 2: THE PLACEHOLDER BLOCK
// ============================================
@Component({
selector: 'app-post',
template: `
<article>
<h1>Post Title</h1>
<p>Post content goes here.</p>
</article>
@defer {
<app-comments [postId]="postId" />
} @placeholder {
<div class="comments-skeleton">
<p>Loading comments...</p>
</div>
}
`,
})
export class PostComponent {}
// ============================================
// PART 3: THE PLACEHOLDER WITH MINIMUM
// ============================================
@defer {
<app-comments [postId]="postId" />
} @placeholder (minimum 500ms) {
<div class="comments-skeleton">
<p>Loading comments...</p>
</div>
}
// ============================================
// PART 4: THE LOADING BLOCK
// ============================================
@defer {
<app-comments [postId]="postId" />
} @placeholder {
<div class="comments-skeleton">
<p>Loading comments...</p>
</div>
} @loading (after 100ms; minimum 1s) {
<app-spinner />
}
// ============================================
// PART 5: THE ERROR BLOCK
// ============================================
@defer {
<app-comments [postId]="postId" />
} @error {
<div class="error-state">
<p>Failed to load comments.</p>
<button (click)="retry()">Try again</button>
</div>
}
// ============================================
// PART 6: THE VIEWPORT TRIGGER
// ============================================
@defer (on viewport) {
<app-comments [postId]="postId" />
} @placeholder {
<div class="comments-placeholder">
<p>Scroll to load comments</p>
</div>
}
// ============================================
// PART 7: THE INTERACTION TRIGGER WITH REFERENCE
// ============================================
<button #showComments type="button">Show all comments</button>
@defer (on interaction(showComments)) {
<app-comments [postId]="postId" />
} @placeholder {
<p>Click the button above to load comments</p>
}
// ============================================
// PART 8: THE PREFETCH PATTERN
// ============================================
@defer (on viewport; prefetch on idle) {
<app-comments [postId]="postId" />
} @placeholder {
<div class="comments-placeholder">
<p>Comments load as you scroll</p>
</div>
} @loading (minimum 200ms) {
<app-spinner />
}
// ============================================
// PART 9: THE COMBINED TRIGGERS
// ============================================
@defer (on viewport; on timer(10s)) {
<app-comments [postId]="postId" />
} @placeholder {
<p>Comments load on scroll or after 10 seconds</p>
}
// ============================================
// PART 10: THE CUSTOM WHEN TRIGGER
// ============================================
// Component with a signal that becomes true when data is ready
export class PostComponent {
postId = 1;
commentsReady = signal(false);
constructor() {
setTimeout(() => this.commentsReady.set(true), 3000);
}
}
// Template
@defer (when commentsReady()) {
<app-comments [postId]="postId" />
} @placeholder {
<p>Waiting for comments to be ready...</p>
}
The ten parts cover the basic defer block, the placeholder block, the minimum placeholder, the loading block, the error block, the viewport trigger, the interaction trigger with reference, the prefetch pattern, combined triggers, and the custom when trigger.
Quick Reference
The @defer Blocks
| Block | Purpose | Dependencies |
|---|---|---|
@defer | Lazy-loaded content | Deferred (separate chunk) |
@placeholder | Content before trigger fires | Eagerly loaded |
@loading | Content during fetch | Eagerly loaded |
@error | Content if load fails | Eagerly loaded |
The Triggers
| Trigger | Fires When | Default Target |
|---|---|---|
idle | Browser is idle | N/A |
viewport | Element enters viewport | Placeholder |
interaction | Click or keydown | Placeholder |
hover | Mouseover or focusin | Placeholder |
immediate | After non-deferred render | N/A |
timer(duration) | After specified time | N/A |
when condition | Condition becomes truthy | N/A |
The Sub-block Parameters
| Parameter | Applies To | Purpose |
|---|---|---|
minimum | @placeholder, @loading | Minimum display time |
after | @loading | Delay before showing loading |
The Prefetch Triggers
| Prefetch | Loads Code When |
|---|---|
prefetch on idle | Browser idle |
prefetch on viewport | Element enters viewport |
prefetch on hover | Mouse hovers |
prefetch on timer(2s) | After 2 seconds |
prefetch when condition | Condition truthy |
Best Practices
✅ Do This:
// Always include a placeholder for the best UX
@defer { <comments /> } @placeholder { <div class="skeleton" /> } // ✅
// Use minimum on placeholder to prevent flickering
@placeholder (minimum 500ms) { <div class="skeleton" /> } // ✅
// Use prefetch on idle with viewport trigger for below-fold content
@defer (on viewport; prefetch on idle) { <heavy-chart /> } // ✅
// Use template reference variables for interaction/viewport targets
<button #btn>Load</button>
@defer (on interaction(btn)) { <content /> } // ✅
// Add an error block with a retry action
@error { <p>Failed</p><button (click)="retry()">Retry</button> } // ✅
❌ Don’t Do This:
// Don't put heavy components in the placeholder
@placeholder { <heavy-chart /> } // ❌ eagerly loaded
// Don't expect the defer block to revert to placeholder
@defer (when condition) { <content /> } // ❌ one-time swap
// Don't use as alias with when
@defer (when items$ | async; as items) // ❌ not supported
// Don't use viewport trigger without placeholder or reference
@defer (on viewport) { <content /> } // ❌ nothing to observe
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Defer never triggers | Viewport trigger has no placeholder or reference | Add placeholder with single root or reference variable |
| Placeholder flickers | No minimum set | Add minimum 500ms to placeholder |
| Loading never shows | Fetch completes before after timer | Adjust after or remove it |
| Content doesn’t revert | Defer is one-time | Wrap in @if for toggle behavior |
as alias fails | Only @if supports as | Move alias to @if wrapper |
| HMR loads eagerly | HMR overrides triggers | Use --no-hmr flag |
Real-World Examples
1. Comments on Viewport
@defer (on viewport) { <app-comments /> } @placeholder { <div class="skeleton" /> }
2. Heavy Chart on Interaction
@defer (on interaction(chartBtn)) { <app-chart /> } @placeholder { <p>Click to load chart</p> }
3. Prefetch on Idle + Viewport Render
@defer (on viewport; prefetch on idle) { <app-recommendations /> }
4. Timer-Based Loading
@defer (on timer(3s)) { <app-newsletter /> } @placeholder { <p>Loading newsletter...</p> }
5. Combined Triggers (OR)
@defer (on viewport; on timer(10s)) { <app-heavy /> }
6. Custom When Condition
@defer (when dataReady()) { <app-data-view /> } @placeholder { <p>Waiting...</p> }
7. Error with Retry
@defer { <app-comments /> } @error { <button (click)="retry()">Retry</button> }
8. Hover Prefetch
@defer (on hover) { <app-tooltip-content /> } @placeholder { <span>Hover me</span> }
9. Immediate Load
@defer (on immediate) { <app-secondary-content /> }
10. Loading with Minimum and After
@defer { <app-comments /> } @loading (after 100ms; minimum 1s) { <app-spinner /> }
Visual: The Defer Block Lifecycle
┌──────────────────────────────────────────────┐
│ DEFER BLOCK LIFECYCLE │
│ │
│ 1. Initial render │
│ └─ @placeholder shown │
│ │
│ 2. Trigger fires (viewport, interaction...) │
│ └─ Loading begins │
│ │
│ 3. @loading block shown (if after elapsed) │
│ └─ Fetch in progress │
│ │
│ 4a. Fetch succeeds │
│ └─ @defer content rendered │
│ │
│ 4b. Fetch fails │
│ └─ @error block shown │
│ │
│ The swap is one-time. No revert. │
│ │
└──────────────────────────────────────────────┘
Visual: Trigger Decision Flow
┌──────────────────────────────────────────────┐
│ TRIGGER DECISION │
│ │
│ Is content above the fold? │
│ ├─ YES → @defer (on immediate) │
│ └─ NO → Is it heavy? │
│ ├─ YES → @defer (on viewport) │
│ └─ NO → @defer (on idle) │
│ │
│ User action expected? │
│ ├─ Click → @defer (on interaction) │
│ └─ Hover → @defer (on hover) │
│ │
│ Want instant render? │
│ └─ @defer (on viewport; prefetch on idle) │
│ │
└──────────────────────────────────────────────┘
Visual: Prefetch vs Render Timing
┌──────────────────────────────────────────────┐
│ WITHOUT PREFETCH │
│ │
│ Viewport enter → Fetch starts → Render │
│ │ │ │ │
│ └── user sees placeholder ──┘ │
│ │
├──────────────────────────────────────────────┤
│ WITH PREFETCH ON IDLE │
│ │
│ Idle → Fetch starts → Code cached │
│ │ │
│ │ (user scrolls) │
│ ▼ │
│ Viewport enter → Render instantly │
│ │
│ No loading state visible to user. │
│ │
└──────────────────────────────────────────────┘
Visual: Sub-block Dependencies
┌──────────────────────────────────────────────┐
│ EAGER vs DEFERRED DEPENDENCIES │
│ │
│ @defer { │
│ <heavy-component /> ← DEFERRED chunk │
│ } │
│ │
│ @placeholder { │
│ <skeleton /> ← EAGER chunk │
│ } │
│ │
│ @loading { │
│ <spinner /> ← EAGER chunk │
│ } │
│ │
│ @error { │
│ <error-msg /> ← EAGER chunk │
│ } │
│ │
│ Keep sub-block content lightweight. │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Introduced | Angular v17 |
| Purpose | Declarative lazy loading in templates |
| Main block | @defer |
| Sub-blocks | @placeholder, @loading, @error |
| Default trigger | idle |
| Triggers | idle, viewport, interaction, hover, immediate, timer, when |
| Prefetch | prefetch on and prefetch when |
| One-time swap | No revert to placeholder |
| Sub-block deps | Eagerly loaded |
| Reference variable | Required for custom trigger targets |
Key takeaways:
@deferdeclares lazy loading at the template level. Angular transforms the static imports inside the block into dynamic imports, the bundler splits them into separate chunks, and the runtime fetches them only when a trigger fires .- Only the
@defercontent is lazy-loaded. The@placeholder,@loading, and@errorblocks are eagerly loaded. Keep their dependencies lightweight . - The default trigger is
idle. For below-the-fold content, useon viewport. For user-initiated content, useon interactionoron hover. - Prefetching loads code before rendering.
@defer (on viewport; prefetch on idle)fetches the JavaScript when the browser is idle and renders when the element enters the viewport, eliminating the visible loading state . - The swap is one-time. Once the defer block fires, it does not revert to the placeholder if the condition becomes false. Wrap in
@iffor toggle behavior . viewportandinteractiontriggers need a target. Either the placeholder with a single root element or an explicit template reference variable .asaliases are not supported inwhentriggers. Move the alias to an@ifblock that wraps the@deferif you need the resolved value .
Remember: @defer is the declarative answer to lazy loading in Angular. It replaces imperative dynamic imports, manual Intersection Observers, and hand-rolled loading states with a single block and a trigger. Use on viewport for below-the-fold content, on interaction for user-initiated loads, and prefetch on idle to eliminate loading states entirely. Always include a placeholder, and keep its dependencies light. The defer block fires once — if you need toggling, use @if. Deferrable views are not just about bundle size; they are about expressing the intent of lazy loading directly in the template, where anyone reading the markup can see when and how content loads.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!