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:
- Trigger: determines when to render the component in the template
- 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 *)
- idle (default)
@defer (on idle) {
<component />
}
// equivalent to:
@defer (on idle; prefetch on idle) {
<component />
}
- 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 />
}
- interaction
// With placeholder
@defer (on interaction) {
<large-component />
} @placeholder {
<placeholder-component />
}
// Without placeholder
<div #title>Title</div>
@defer (on interaction(title)) {
<large-component />
}
- hover
@defer (on hover) {
<detailed-preview />
} @placeholder {
<preview-card />
}
- immediate
@defer (on immediate) {
<critical-notice />
}
- 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:
- If you need to support older versions, you can implement a workaround using type-only imports:
- Example from Angular Love Autumn Camp by Dawid Kostka
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! 👋😁
