A Comprehensive Guide to Angular’s Defer Block

Netanel Basal
Netanel Basal
Published in
8 min readOct 8, 2023

--

With the latest control flow enhancements, Angular v17 introduces an impressive and highly beneficial feature: the defer block.

The primary purpose of the defer block is to lazy load content. Whether it’s a component, directive, or pipe, if it’s placed inside a defer block, Angular will only load it based on the specified conditions or events. This is particularly useful for performance optimization, especially when certain components aren’t immediately needed or visible to the user.

Consider this simple component:

@Component({
selector: 'my-cmp',
standalone: true,
template: 'Hi!',
})
class MyCmp { }

Now, let’s look at how the defer block can be applied:


@Component({
standalone: true,
imports: [MyCmp, FooDirective, BarPipe],
template: `
@defer (when isVisible) {
<my-cmp appFoo/>
{{ 'foo' | bar }}
}

<button (click)="isVisible = true">Load</button>
`
})
export class AppComponent {
isVisible = false;
}

In this example, the defer block is paired with the when condition, which expects a boolean value. Angular's compiler processes this by breaking down the component, directive, and pipe into separate chunks.

They are then loaded and rendered only when isVisible is true:

Additionally, when integrating the defer block with basic HTML content, it takes on capabilities reminiscent of an advanced ngIf directive. This becomes particularly evident when combined with the on condition, a feature we'll delve into in greater detail shortly.

The defer block can be further augmented with the @loading, @placeholder, and @error blocks to provide a more comprehensive user experience:

@Component({
template: `
@defer (when isVisible) {
<my-cmp />
}
@loading {
Loading...
} @placeholder {
Placeholder
} @error {
Failed to load dependencies
}
`
})
class AppComponent { ... }
  • @loading: Displays the specified content during the loading phase of dependencies.
  • @placeholder: Displays the given content as an interim display until the content completes rendering.
  • @error: Displays the specified content if there’s an issue loading content’s dependencies.

The on condition

Angular’s defer functionality is not just limited to simple boolean checks. It introduces the versatile “on” condition, which offers a spectrum of options for component loading and rendering. Let’s dive deeper into its capabilities.

Immediate Rendering with “On Immediate” Condition

@Component({
standalone: true,
imports: [MyCmp],
template: `
@defer (on immediate) {
<my-cmp />
}
`
})
class AppComponent { }

In the code above, the combination of the @defer block with the "on immediate" condition guarantees that the component is instantly rendered once it undergoes lazy-loading.

Idle Rendering with “On Idle” Condition

@Component({
standalone: true,
imports: [MyCmp],
template: `
@defer (on idle) {
<my-cmp />
}
`
})
class AppComponent { }

Here, the component is loaded and rendered during browser idle times, leveraging the requestIdleCallback API.

Deferring Rendering Based on User Interaction

One of the powerful features of Angular’s defer functionality is the ability to delay the rendering of a component until a specific interaction occurs. This is particularly useful for scenarios where you might want to load content only when the user interacts with a specific element, such as a button click:

@Component({
standalone: true,
template: `
@defer (on interaction(trigger)) {
<my-cmp />
}
@placeholder {
Placeholder
}

<button #trigger>Trigger</button>
`
})
class AppComponent { }

In this example, the component loading and rendering is deferred and will only render once the button, denoted by the #trigger reference, is clicked. Until that interaction, the @placeholder block showcases the "Placeholder" content. Supported interaction events include click, focus, touch, and input.

In certain situations, you might not want to tie the defer block to a specific element. Angular addresses this by allowing deferred content to load based on interactions with the placeholder itself:

@Component({
standalone: true,
template: `
@defer (on interaction) {
Main content
} @placeholder {
<button>Trigger</button>
}
`
})
class AppComponent {}

Hover-Based Rendering

Another versatile feature of Angular’s defer block is the ability to defer the rendering of content until a specific element is hovered over:

@Component({
standalone: true,
template: `
@defer (on hover(trigger)) {
Main content
} @placeholder {
Placeholder
}

<button #trigger>Trigger</button>
`
})
class AppComponent { }

The @defer (on hover(trigger)) block activates the rendering of the “Main content” exclusively when the button, denoted by the #trigger template reference, is moused over.

Timer-Driven Rendering

Angular’s defer block also provides the capability to delay the rendering of content based on a timer. This can be particularly useful in scenarios where you want to stagger the loading of multiple components or introduce a delay before a component is rendered, enhancing the user experience with controlled and timed content display:

@Component({
standalone: true,
template: `
@defer (on timer(1500ms)) {
Main content
} @placeholder {
Placeholder
}
`
})
class AppComponent { }

Viewport-Based Rendering

Angular’s defer block provides a built-in mechanism for this through the on viewport condition. This allows developers to defer the rendering of content until a specific element enters the viewport:

@Component({
standalone: true,
template: `
@defer (on viewport(trigger)) {
<my-comp />
} @placeholder {
Placeholder
}

<div #trigger style="margin-top: 1500px">Content</div>
`
})
class AppComponent { }

The @defer (on viewport(trigger)) block ensures that the component is rendered only when the div (identified by the #trigger template reference variable) enters the viewport.

This viewport-triggered deferment is a powerful tool for developers aiming to optimize resource loading based on the user’s scroll position. It ensures that content is loaded and rendered only when necessary, reducing initial load times and improving overall performance.

While specifying an explicit trigger for the on viewport condition can be useful, there are scenarios where developers might want the deferred content to be loaded based on the visibility of the placeholder itself. Angular's defer block supports this through an implicit viewport trigger mechanism:

@Component({
standalone: true,
template: `
@defer (on viewport) {
<my-comp />
} @placeholder {
<div>Placeholder</div>
}
`
})
class AppComponent { }

The @defer (on viewport) block is set up to render the component when the placeholder content enters the viewport. The placeholder content in this case is a div element with the label “Placeholder”. This div acts as the implicit trigger for the viewport condition.

Optimizing with Prefetching

In the realm of modern web development, performance is paramount. Prefetching, or preloading resources before they’re needed, is a strategy to bolster performance. Angular’s defer block natively supports prefetching, ensuring resources are primed and ready, thus minimizing wait times and elevating user experience.

The prefetch option seamlessly integrates with all the functionalities we’ve explored using the when or on conditions.

Basic Prefetching

In the example below, the @defer block is combined with both the when and prefetch conditions. The component <my-comp /> will be rendered when isVisible is true. Additionally, the component chunk will be prefetched when prefetchCondition is true.

@Component({
standalone: true,
imports: [MyComp],
template: `
@defer (when isVisible; prefetch when prefetchCondition) {
<my-comp />
} @placeholder {
Placeholder
}
`
})
class AppComponent {
isVisible = false;
prefetchCondition = false;
}

Immediate Prefetching

In the next example, the prefetch on immediate condition ensures that the the component chunk is loaded immediately, even before it’s needed:

@Component({
standalone: true,
imports: [MyComp],
template: `
@defer (when isVisible; prefetch on immediate) {
<my-comp />
} @placeholder {
Placeholder
}
`
})
class AppComponent {
isVisible = false;
}

Idle Prefetching

Here, the prefetch on idle condition ensures that the component chunk is loaded during the browser's idle times. This is achieved using the requestIdleCallback API, which allows tasks to be scheduled during idle periods:

@Component({
standalone: true,
imports: [MyComp],
template: `
@defer (when isVisible; prefetch on idle) {
<my-comp />
} @placeholder {
Placeholder
}
`
})
class AppComponent {
isVisible = false;
}

Timer-Based Prefetching

In this example, the prefetch on timer(100ms) condition ensures that the the component is loaded after a delay of 100 milliseconds. This can be particularly useful when you want to introduce a slight delay before prefetching resources, allowing other critical resources to load first.

@Component({
standalone: true,
imports: [MyCmp],
template: `
@defer (when isVisible; prefetch on timer(100ms)) {
<my-comp />
} @placeholder {
Placeholder
}
`
})
class AppComponent {
isVisible = false;
}

Viewport-Based Prefetching

@Component({
standalone: true,
imports: [MyCmp],
template: `
@defer (when isVisible; prefetch on viewport(trigger)) {
<my-comp />
} @placeholder {
Placeholder
}
`
})
class AppComponent {
isVisible = false;
}

Advanced Conditions: Minimum and After

These conditions offer granular control over the display duration of loading and placeholder states:

@Component({
standalone: true,
imports: [MyCmp],
template: `
@defer (when isVisible; prefetch when prefetchTrigger) {
<my-cmp />
}
@loading (after 100ms; minimum 150ms) {
Loading
}
@placeholder (minimum 100ms) {
Placeholder
}
@error {
Error
}
`
})
class AppComponent {
isVisible = false;
}
  • after 100ms: The loading state appears after a 100ms delay.
  • minimum 150ms: The loading state persists for at least 150ms.
  • minimum 100ms: The placeholder remains visible for a minimum of 100ms.

Note: The @loading state is bypassed when resources have been prefetched.

Integrating the defer Block within the for of Loop

The defer block can be seamlessly incorporated within the for of loop, enhancing the dynamic rendering of components based on conditions. Here's a demonstration:

@Component({
standalone: true,
imports: [MyCmp],
template: `
@for (item of items; track item) {
@defer (on timer(500ms)) {
<my-cmp />
} @placeholder {
Placeholder for {{ item }}
}
}
`
})
class AppComponent {
items = [...];
}

In the example above, for each item in the items array, the defer block introduces a delay of 500ms before rendering the <my-cmp /> component. Meanwhile, a placeholder specific to the current item is displayed.

Incorporating the defer Block with Content Projection

The defer block integrates effortlessly with content projection, enhancing the dynamic inclusion of content. Here's an example:

@Component({
selector: 'my-cmp',
standalone: true,
imports: [BazComponent],
template: `
@defer (when isVisible) {
<app-baz />
<ng-content />
}

<button (click)="isVisible = true">Show</button>
`,
})
export class MyCmp {
isVisible = false;
}


@Component({
standalone: true,
imports: [MyCmp],
template: `
<my-cmp>
my content
</my-cmp>
`
})
class AppComponent { }

Let’s conclude with a practical example: a stepper that prefetches and lazy-loads its steps during idle times:

@Component({
standalone: true,
imports: [MyCmp],
template: `
@if (step === 0) {
<app-step-one />
<button (click)="updateStep(1)">Next</button>
}

@defer (prefetch on idle) {
@if (step === 1) {
<app-step-two />
<button (click)="updateStep(2)">Next</button>
}
}


@defer (prefetch on idle) {
@if (step === 2) {
<app-step-three />
}
}
`
})
class AppComponent {
step = 0;

updateStep(step: number) {
this.step = step;
}
}

Follow me on Medium or Twitter to read more about Angular and JS!

--

--

A FrontEnd Tech Lead, blogger, and open source maintainer. The founder of ngneat, husband and father.