A Comprehensive Guide to Angular’s Defer Block
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!