The Importance of Fast Initial Loads
While Angular 17 brought several SSR-related refinements—stabilizing hydration, renaming the universal package to @angular/ssr, and integrating SSR into the ng new command—the headline feature for those focused on performance is the new control flow syntax, specifically the @defer block. The updated template syntax also improves runtime efficiency, a topic we will explore in a separate post.
Prior to Angular 16, lazily loading components (now called deferring) required somewhat convoluted workarounds. Developers had to rely on the router's loadChildren for modules or arrays of components, or loadComponent for individual standalone components. Although ComponentFactoryResolver became unnecessary in Angular 13, the process still demanded manual wiring with a ViewContainerRef and an async / await block to fetch, instantiate, and attach the component to the DOM.
The @defer block changes this entirely, offering a clean, declarative way to postpone component loading until absolutely required. This is particularly valuable for components that sit outside the user's immediate viewport, often called above-the-fold. The technique yields the most significant gains when applied to heavyweight components—those that pull in large third-party libraries for feature-rich tables, charts, or PDF generation. Deferring these also removes their associated packages from the eagerly-loaded main bundle.
We've previously discussed why initial load performance matters in the context of SSR. Deferrable Views are a lightweight strategy to shrink the initial bundle, directly impacting key metrics like First Contentful Paint (FCP), Largest Contentful Paint (LCP), and even Time to First Byte (TTFB).
However, be mindful of Cumulative Layout Shift (CLS). Deferring any component that is visible on the first screen can cause layout jumps. To counter this, use the @placeholder and @loading blocks with fixed dimensions, in the same way you would reserve space for lazy-loaded images with NgOptimizeImage.
Dynamic Loading in the Angular 13-16 Era
The previous approach to dynamic component loading, which I've covered in my Performance Workshop, involved several steps.
First, you needed a ViewContainerRef in the template. Using an ng-container avoids creating an extra DOM element:
<ng-container #viewContainer />
A helpful note: Since Angular 15.1.0, self-closing tags are valid for components, even those without content.
Second, in the component class, you'd use a @ViewChild to access that ViewContainerRef via the template reference #viewContainer:
@ViewChild('viewContainer', { read: ViewContainerRef }) viewContainerRef!: ViewContainerRef;
Third, an async / await block handles the dynamic import and DOM insertion:
async ngAfterViewInit() {
const { LazyComponent } = await import('./lazy/lazy.component');
const lazyComponentRef = this.viewContainerRef.createComponent(LazyComponent);
}
This method, while functional, is verbose. The Angular Compiler does generate a separate chunk for the LazyComponent, which the browser fetches on demand, but the new @defer syntax is far more elegant.
Important: For any of these deferral techniques to work, the target LazyComponent must be a standalone component.
Simplified Deferral with Angular 17
Angular 17 introduces the intuitive @defer syntax, which looks similar to the new @if and is significantly easier to use:
@defer {
<aa-lazy-component />
}
That's the core of it. The compiler handles code splitting automatically, generating a new chunk for LazyComponent that the browser loads when needed.
But there's more. The real power of @defer lies in its control mechanisms—triggers that let you dictate exactly when the lazy loading begins.
Mastering Triggers
The fundamental purpose of a @defer block is to swap out placeholder content for the lazily-loaded content. Two primary attributes control this swap: on and when.
Using the on Trigger
The on keyword allows you to specify built-in trigger conditions, which are event-based.
@defer (on viewport) {
<aa-lazy-component />
}
Angular 17 offers several built-in on events:
on idle– Fires when the browser's main thread is idle, utilizing the requestIdleCallback API. This is the default behavior, so it doesn't need to be stated explicitly.@defer { <aa-lazy-component /> }on viewport– Triggers when the specified content enters the viewport, tracked via the IntersectionObserver API. By default, it observes the placeholder content itself.@defer (on viewport) { <aa-lazy-component /> } @placeholder { <img width="420" height="420" alt="lazy placeholder" src="placeholder.avif" /> }- For more control, you can specify a different element to observe using a template reference like
#variable.
<div #viewportVariable>Hello!</div> @defer (on viewport(viewportVariable)) { <aa-lazy-component /> }- For more control, you can specify a different element to observe using a template reference like
on interaction– Activates when the user interacts with the specified element viaclickorkeydownevents.@defer (on interaction) { <aa-lazy-component /> }- Again, you can provide a template reference
#variableto target a specific element.
<button #interactionVariable>Hello!</button> @defer (on interaction(interactionVariable)) { <aa-lazy-component /> }- Again, you can provide a template reference
on hover– Responds to a hover action, which is internally mapped tomouseenterorfocusinevents.@defer (on hover) { <aa-lazy-component /> }- As with other triggers, you can specify a template reference
#variable.
<div #hoverVariable>Hello!</div> @defer (on viewport(hoverVariable)) { <aa-lazy-component /> }- As with other triggers, you can specify a template reference
on immediate– Starts the deferred load right away. Once the client finishes rendering, the chunk is fetched immediately, much like asetTimeoutwith a0millisecond delay.@defer (on immediate) { <aa-lazy-component /> }on timer(– Triggers after a specified timeout in) msors. It behaves like asetTimeoutthat begins once client-side rendering completes.@defer (on timer(4200ms)) { <aa-lazy-component /> }
Employing the when Trigger
The when keyword accepts an expression that returns a boolean. It's a one-time operation; if the condition becomes true, the content loads and will not revert even if the condition later returns to false.
class WhenDemoComponent {
condition = false;
trigger() {
this.condition = true;
}
}
@defer (when condition) {
<aa-lazy-component />
}
You can combine multiple when and on triggers. These are treated as OR conditions—the swap occurs when any of the specified conditions are met.
Beyond Basic Deferral
In addition to simple triggers, the @defer block includes several features for a refined user experience.
Proactive Loading with prefetch
The @defer block lets you define conditions for prefetching dependencies before the main trigger executes. This uses the same when / on syntax.
For instance, the following code starts fetching the deferred bundle as soon as the browser is idle, well before the content is likely to be needed:
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
}
Showing Initial Content with @placeholder
If you want to display content before loading commences, the @placeholder block is your tool. By default, defer blocks are inactive until triggered. This block provides something to render during that inactive period, and its content is eagerly loaded along with the main bundle.
Important consideration: During server-side rendering (SSR) or static site generation (SSG), @defer blocks ignore triggers and always render the @placeholder content. If no placeholder exists, nothing renders.
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
} @placeholder (minimum 500ms) {
<img width="420" height="420" alt="lazy placeholder" src="placeholder.avif" />
}
You can also use the minimum parameter within @placeholder to define a minimum display duration.
Indicating Progress with @loading
While the deferred dependencies are actually being fetched and processed, the @loading block displays its content, such as a spinner. Like @placeholder, its dependencies are eagerly loaded.
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
} @placeholder (minimum 500ms) {
<img width="420" height="420" alt="lazy component placeholder" src="placeholder.avif" />
} @loading (after 500ms; minimum 1s) {
<img width="420" height="420" alt="lazy is loading spinner" src="spinner.avif" />
}
To avoid a flickering effect when loads are fast, @loading also supports the after and minimum parameters, delaying the loading indicator's appearance and setting its minimum display time.
Handling Failures with @error
In the event of a network error or other loading failure, the @error block provides fail-safe content to display to the user.
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
} @placeholder (minimum 500ms) {
<img width="420" height="420" alt="lazy component placeholder" src="placeholder.avif" />
} @loading (after 500ms; minimum 1s) {
<img width="420" height="420" alt="lazy is loading spinner" src="spinner.avif" />
} @error {
<p>Why do I exist?</p>
}
Seamless Migration with CLIs
One of the Angular team's objectives for the built-in control flow was to make migration fully automated.
You can test this in your own project with just two commands:
ng update
ng g @angular/core:control-flow
After running these, be sure to explore the new @defer feature.
The Performance Workshop Advantage
For a deeper dive into Angular and its performance capabilities, we offer several workshops in both English and German, covering best practices and accessibility alongside performance topics.
Bringing It All Together
Angular 17's Deferrable Views, powered by the @defer block, represent a major step forward in simplifying how we load standalone components. The feature shines not only in its simplicity but also in its performance impact, allowing developers to keep heavy components and their third-party dependencies out of the initial JavaScript bundle until they're truly necessary.
Combining the built-in on triggers with the custom when condition, alongside utility features like prefetch and the structural blocks for @placeholder, @loading, and @error, provides a complete toolkit for building faster, more responsive, and more resilient web applications.
Sources & Further Reading
- Why is Initial Load Performance so Important? by Alexander Thalhammer
- Angular Update Guide to V17 incl. migrations by Alexander Thalhammer
- What's new in Angular 17 by Manfred Steyer
- Introducing Angular 17 by Minko Gechev
- Deferrable Views with Jessica Janiuk on Angular YouTube
- Deferrable Views in the Angular Docs
- Complete Guide for Server-Side Rendering (SSR) in Angular by Alexander Thalhammer
Authored by Alexander Thalhammer. Connect with me on Linkedin, X, or GitHub.
