| |

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

BlockPurposeDependencies
@deferLazy-loaded contentDeferred (separate chunk)
@placeholderContent before trigger firesEagerly loaded
@loadingContent during fetchEagerly loaded
@errorContent if load failsEagerly loaded

The Triggers

TriggerFires WhenDefault Target
idleBrowser is idleN/A
viewportElement enters viewportPlaceholder
interactionClick or keydownPlaceholder
hoverMouseover or focusinPlaceholder
immediateAfter non-deferred renderN/A
timer(duration)After specified timeN/A
when conditionCondition becomes truthyN/A

The Sub-block Parameters

ParameterApplies ToPurpose
minimum@placeholder, @loadingMinimum display time
after@loadingDelay before showing loading

The Prefetch Triggers

PrefetchLoads Code When
prefetch on idleBrowser idle
prefetch on viewportElement enters viewport
prefetch on hoverMouse hovers
prefetch on timer(2s)After 2 seconds
prefetch when conditionCondition 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

PitfallWhy It HappensFix
Defer never triggersViewport trigger has no placeholder or referenceAdd placeholder with single root or reference variable
Placeholder flickersNo minimum setAdd minimum 500ms to placeholder
Loading never showsFetch completes before after timerAdjust after or remove it
Content doesn’t revertDefer is one-timeWrap in @if for toggle behavior
as alias failsOnly @if supports asMove alias to @if wrapper
HMR loads eagerlyHMR overrides triggersUse --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

ItemValue
IntroducedAngular v17
PurposeDeclarative lazy loading in templates
Main block@defer
Sub-blocks@placeholder, @loading, @error
Default triggeridle
Triggersidle, viewport, interaction, hover, immediate, timer, when
Prefetchprefetch on and prefetch when
One-time swapNo revert to placeholder
Sub-block depsEagerly loaded
Reference variableRequired for custom trigger targets

Key takeaways:

  • @defer declares 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 @defer content is lazy-loaded. The @placeholder, @loading, and @error blocks are eagerly loaded. Keep their dependencies lightweight .
  • The default trigger is idle. For below-the-fold content, use on viewport. For user-initiated content, use on interaction or on 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 @if for toggle behavior .
  • viewport and interaction triggers need a target. Either the placeholder with a single root element or an explicit template reference variable .
  • as aliases are not supported in when triggers. Move the alias to an @if block that wraps the @defer if 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!