Angular 17 introduced a powerful feature called Deferrable Views (stable in Angular 18), revolutionising how we handle lazy loading in our applications. While traditionally we relied on dynamic imports with async/await and ViewContainerRef, Deferrable Views bring lazy loading directly to our templates with a much more elegant and flexible approach.

Evolution of Lazy Loading in Angular

Traditional Approach: Dynamic Imports

Before Deferrable Views, implementing lazy loading at the component level required manual configuration through dynamic imports or router-level dynamic loading. Here's how it typically looked:

@Component({
  template: "<ng-container #container />",
})
export class ParentComponent implements AfterViewInit {
  @ViewChild("container", { read: ViewContainerRef })
  container!: ViewContainerRef;

  async ngAfterViewInit(): void {
    const { LazyComponent } = await import("./lazy.component");
    this.container.createComponent(LazyComponent);
  }
}

This approach, while functional, had several limitations:

  • Required manual handling of the ViewContainerRef
  • Lacked fine-grained control over loading conditions
  • Mixed concerns between component logic and lazy loading

Understanding Deferrable Views

Key Concepts

Deferrable Views introduce two fundamental concepts:

  1. Trigger: determines when to render the component in the template
  2. Prefetch: controls when to load the bundle from the server

These concepts are distinct and can be used independently:

  • You might want to prefetch a component early but delay its rendering
  • Or trigger rendering immediately once specific conditions are met

Basic Implementation

The simplest implementation of a Deferrable View looks like this:

@Component({
  template: `
    @defer {
      <heavy-component />
    }
  `
})

By default, this will:

  • Create a separate bundle for heavy-component
  • Load when the browser is idle (after all page resources have been loaded)
  • Render once loading is complete

Important Restrictions

When working with Deferrable Views, keep in mind:

  • Only works with standalone components!
  • Deferred components shouldn't export tokens/constants used elsewhere (breaks lazy-loading)
  • Must be used in the parent's template (not with ViewChild)
  • Components imported by the deferred component can be either standalone or NgModule-based

Advanced Features

Control Blocks

Deferrable Views come with three powerful control blocks:

1. @placeholder

@defer {
  <heavy-chart />
} @placeholder {
  <loading-skeleton />
}
  • Shows initial content while the main component loads
  • Included in the main bundle, but it might be another one (for example if you have nested deferred blocks)
  • Optional minimum display time: @placeholder (minimum 2s)
  • Content inside @placeholder block is eagerly loaded

2. @loading

@defer {
  <data-visualization />
} @loading {
  <spinner />
}
  • Displays while the bundle is loading
  • Included in the main bundle
  • Supports timing parameters:
    • @loading (minimum 2s)
    • @loading (after 1s)
    • Combined: @loading (after 1s; minimum 2s)
  • You probably won't see the loading block if you have after AND fast loading times

3. @error

@defer {
  <complex-component />
} @error {
  <error-state />
}
  • It doesn't protect from runtime errors and also there is no way to retrigger the loading after a network failure.
  • Content inside @error block is eagerly loaded.

How do @placeholder and @loading differ?

While @placeholder and @loading might appear similar, they handle distinct moments in the deferred content lifecycle. @placeholder acts as a pre-loading visual, shown immediately, even before bundle loading begins. @loading, conversely, appears only during the active bundle download, disappearing once the content is ready. Essentially, @placeholder holds the spot, while @loading signals active retrieval.

Trigger Types

Predefined Triggers (used with on *)

  1. idle (default)
@defer (on idle) {
  <component />
}

// equivalent to:
@defer (on idle; prefetch on idle) {
  <component />
}
  1. viewport
// With placeholder
@defer (on viewport) {
  <infinite-scroll-content />
} @placeholder {
  <loading-indicator />
}

// Without placeholder
<div #title>Title</div>
@defer (on viewport(title)) {
  <infinite-scroll-content />
}
  1. interaction
// With placeholder
@defer (on interaction) {
  <large-component />
} @placeholder {
  <placeholder-component />
}

// Without placeholder
<div #title>Title</div>
@defer (on interaction(title)) {
  <large-component />
}
  1. hover
@defer (on hover) {
  <detailed-preview />
} @placeholder {
  <preview-card />
}
  1. immediate
@defer (on immediate) {
  <critical-notice />
}
  1. timer
@defer (on timer(5s)) {
  <delayed-content />
}

Note: Duplicate triggers of the same type are not allowed within a single deferrable view.

Custom Triggers (used with when *)

Use when for custom conditions:

@Component({
  template: `
    @defer(when showDetails; prefetch when isNearBottom) {
    <detailed-view />
    }
  `,
})
export class MyComponent {
  showDetails = signal(false);
  isNearBottom = computed(() => this.scrollPosition() > 0.8);
}

Performance Optimization Patterns

Multiple Components in One Block

@defer (on viewport) {
  <statistics-chart />
  <data-table />
  <export-options />
}
  • Each component gets its own chunk
  • Loaded together but independently bundled

Strategic Prefetching

// Load it when we are on the viewport and show it when we interact
@defer (on interaction; prefetch on viewport) {
  <comments-section />
} @placeholder {
  <comments-preview />
}

// Load it when load variable is true and show it when show variable is true
@defer(when show; prefetch when load) {
  <large-component />
}

ViewChild and Defer Block Compatibility

Prior to Angular 18.2.1, developers encountered an issue where the deferrable view would not work properly in certain scenarios. This problem specifically occurred when referencing heavy (third-party) components outside of parent component imports.

When components were referenced outside the parent component imports, it interfered with proper tree-shaking functionality, preventing the deferrable view from working as intended.

The fix involves two key approaches:

  • Version Update Solution:

    • Upgrading to Angular 18.2.1 or later resolves this issue out of the box
    • This version includes specific fixes for deferrable view functionality
  • Type-Only Import Solution:

import { Component, computed, viewChild } from "@angular/core";
import {
  ChartComponent,
  type ChartComponent as ChartComponentType, // solution Angular < 18.2.1
} from "./chart.component";

@Component({
  selector: "app-parent",
  standalone: true,
  imports: [ChartComponent],
  template: `
    @defer (on viewport; on idle) {
    <app-chart #child />
    } @placeholder (minimum 1s) { ... }
  `,
})
export class ParentComponent {
  readonly chart = viewChild<ChartComponentType>("child");
  readonly chartId = computed(() => this.chart()?.id);
}

@for inside vs outside deferrable view

The recommendation is to place @defer outside @for when displaying uniform components, as it creates fewer views. However, use deferrable view inside @for when you need different heavy components rendered conditionally within the loop.

@for (item of items) {
  @defer {
    <heavy-component />
  } 
}

@defer {
  @for (item of items) {
    <heavy-component />
  }
}

The choice impacts performance through the number of embedded views and deferrable views Angular needs to manage at runtime.

To learn more about techincal aspects you can check Matthieu Riegler post.

SSR Considerations

When using Deferrable Views with Server-Side Rendering:

  • Browser events aren't available during SSR
  • Only @placeholder blocks render on the server
  • Triggers activate post-hydration
  • Plan your initial loading state carefully

Incremental hydration

Incremental Hydration extended the behavior of @defer (experimental in v19). Jessica Janiuk started an RFC that leverages @defer's capabilities to control hydration timing.

Under the hood:

  • Deferrable views define hydration boundaries
  • Server renders static HTML first (both eager and defered components)
  • Client hydrates components based on triggers:
@defer (hydrate on viewport) {
  <component />
}

// On viewport
@defer (hydrate when condition) {
  <component />
}

// When condition met (static only)
@defer (hydrate never) {
  <component />
}

Best Practices

Performance Optimization

  • Place deferrable views outside loops when possible
  • Use prefetch strategically for better UX
  • Keep placeholder content light

User Experience

  • Always provide meaningful placeholder content
  • Consider loading states carefully
  • Handle errors gracefully

Code Organization

  • Group-related deferred components
  • Keep trigger logic clean and maintainable
  • Document loading strategies

Conclusion

Deferrable Views represent a significant step forward in Angular's lazy loading capabilities. They provide:

  • More granular control over component loading
  • Better user experience through built-in loading states
  • Cleaner, more declarative code

Now stable as of Angular 18, this feature provides a robust foundation for building more performant and user-friendly Angular applications.

Remember that Deferrable Views load parts of a component template, while the lazy-loading mechanism loads a whole component. Also, Deferrable Views are not linked to the Router.

Thanks for reading so far 🙏

Spread the Angular love! 💜

If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥

Thanks for being part of this Angular journey! 👋😁


Tagged in:

Articles

Last Update: June 27, 2025