@Component({
selector: "large-component",
template: ` <h2>large-component is displayed...</h2> `,
})
export class ExampleComponent {}
@Component({
selector: "app",
template: `
<h2>Some content that will always be displayed ...</h2>
@defer {
<large-component />
}
`,
})
export class AppComponent {}
@defer {
<large-component />
}
@placeholder {
<initial-content />
}
@defer {
<large-component />
}
@placeholder (minimum 2s) {
<initial-content />
}
What is @defer, and why do we need it?
In single-page applications, loading every piece of code upfront, regardless of whether it will actually be used, often results in slower initial loads and wasted bandwidth. A more efficient strategy is to prioritize critical resources while postponing the download and execution of non-essential parts of the application.
Router-based lazy loading has long been the primary tool in Angular for this purpose. However, it has a significant constraint: it operates at the route level, making it impossible to lazily load individual chunks of a single template or component.
Consider a scenario where a page contains a large, dependency-heavy section that isn't immediately visible to the user. You might want to load that section's code in the background while the user reads the main content, so it's ready when they scroll down or click a button. This granular level of control isn't achievable with router-based lazy loading.
This is precisely the gap that the @defer syntax fills. It enables fine-grained deferred loading directly within your templates, offering a level of control that goes far beyond what's possible with route-level code splitting.
Furthermore, @defer provides two separate dimensions of control: prefetching (when the code bundle is fetched from the server) and rendering (when the fetched code is applied to the page). These two operations don't have to happen simultaneously. For instance, you can preload a large component's code in the background, anticipating user interaction, but delay its actual display until a specific condition is met, like a button click.
To manage this process, @defer allows you to define different triggers for both the prefetching and rendering steps. You can use the built-in triggers for common cases or define custom ones for more tailored logic. This flexibility makes @defer a powerful and versatile feature for optimization.
@defer in action
Imagine a component that always displays a portion of its content, but also has a section that's only relevant under certain circumstances. This non-essential section contains a large component, perhaps one that relies on heavy charting libraries or other complex dependencies.
To defer the loading of this component, you wrap it in a @defer block. This single action signals to Angular that the code for this section should be separated into its own JavaScript bundle, separate from the main application bundle.
The impact of this is immediately visible in the build output. You'll notice a new, separate bundle is generated, containing the code for your large component and its dependencies. This extraction shrinks the size of the main bundle, which can lead to a faster initial page load. The new bundle will only be fetched when the @defer block is triggered.
When no explicit trigger is defined, @defer uses the idle trigger by default. This means the browser will wait until it's idle—after all initial page resources have been downloaded and the loading spinner has stopped—before fetching and rendering the deferred content. While this results in the deferred content appearing shortly after the initial render, it also means the critical parts of the application are not blocked by the loading of non-essential components.
Here’s a visual breakdown of the sequence: the main application bundle loads first. Then, when the browser becomes idle, the separate @defer bundle is fetched. Finally, after the fetch is complete, the large component is rendered in the place where the @defer block is located. This process, visible in the network tab, clearly shows the efficient loading of resources in the background.
This establishes the fundamental mechanics of @defer. However, to truly harness its power, you need to answer several key questions:
- How can you control the exact moment the deferred bundle is downloaded?
- How can you control when the large component is finally rendered?
- Can you show a loading indicator while the bundle is being fetched?
- Can you display a meaningful error message if the bundle fails to load?
- Can you provide some initial placeholder content before the deferred code is ready?
To address these concerns, we need to introduce the companion blocks that work alongside @defer: @placeholder, @loading, and @error.
@defer with @placeholder
Sometimes, you might want to leave a temporary, blank space where the @defer block will eventually be placed. However, there are many other situations where providing some initial visual feedback is preferable. The @placeholder block allows you to define this initial state.
The content inside the @placeholder block is displayed immediately in the location where @defer is used. Once the condition for the @defer block is met and the code is loaded, this placeholder content is replaced by the content of the @defer block itself. This provides a seamless transition for the user, who initially sees some basic content and then the fully interactive deferred component.
Important Consideration: Any component, directive, or pipe used within the @placeholder block is considered eagerly loaded and will be a part of the main application bundle. Therefore, it's crucial to keep the placeholder very simple and lightweight. Using heavy dependencies here would defeat the entire purpose of deferring the load. The placeholder should merely serve as a visual cue, letting the user know that content will soon appear in that space.
@placeholder parameters
The @placeholder block supports an optional parameter called minimum. This parameter, specified in seconds or milliseconds, defines the minimum duration for which the placeholder must be displayed before the @defer content can replace it.
For example, a value of 2s would force the placeholder to remain visible for at least two seconds. This feature is essential for preventing a poor user experience. Without a minimum duration, if the network is very fast, the placeholder might flash on the screen for only a split second before being replaced. This flickering can appear as a glitch or bug to the user.
By enforcing a minimum display time, the minimum parameter ensures the placeholder is shown long enough for the user to consciously register it, understanding that more content is on its way. This creates a smoother, more professional-feeling loading experience, avoiding the confusing flash-of-incomplete-content.
Pairing @defer with the @loading block
The purpose of the @loading block is to provide temporary content while the @defer block is actively fetching its associated JavaScript bundle.
Example:
@defer {
<large-component />
}
@loading {
<loading-spinner />
}
During the bundle download, the <loading-spinner /> remains visible. Once the fetch is complete, the spinner is removed and the <large-component /> is inserted into the view.
Keep in mind that all dependencies used inside the @loading block are also included in the initial eager bundle, much like with @placeholder.
The @loading block supports two configurable settings: minimum and after.
-
The
minimumparameter dictates the shortest duration the@loadingblock remains on display. -
The
afterparameter establishes a delay before the@loadingblock becomes visible, counting from the moment loading starts.
You can provide these values using either seconds or milliseconds. Both settings are designed to prevent distracting blinking or flickering in the UI.
Consider this example of how to use them together:
@defer {
<large-component />
} @loading (after 1s; minimum 2s) {
<loading-spinner />
}
- The system observes a 1-second delay post-load initiation before rendering the
@loadingblock's content. - Once shown, the
@loadingcontent stays on screen for a minimum of 2 seconds before<large-component />is revealed.
If the download finishes quicker than the after duration, the @loading content will never appear.
This leads to the following expected behaviors:
-
the loading indicator appears only if the fetch exceeds 1 second; otherwise, it remains hidden
-
if the indicator does appear but the fetch completes, it is prevented from vanishing instantaneously
-
a
minimumdisplay time of 2 seconds is enforced, meaning that the indicator, once shown, is guaranteed to be visible for that entire period.
This approach ensures the indicator isn't a fleeting visual element, which might otherwise seem like a bug to the user.
While they may appear interchangeable, these two blocks fulfill distinct roles.
The @placeholder block is shown first, remaining in place until the @defer block's content is fully prepared for rendering.
This block appears prior to the initiation of the bundle download. For instance, the loading action might be set to happen only after a user click.
Before that trigger event occurs, @placeholder provides preliminary content for the user.
Conversely, the @loading block only appears once the download for the @defer bundle has started and is in progress.
It disappears as soon as the loading process wraps up.
Using @error with @defer
What happens if the bundle download encounters an issue, like a network failure?
The @error block is the designated fallback for a failed attempt to load the @defer block's components.
Example:
@defer {
<large-component />
} @error {
<error-message />
}
Now that we have a good grasp on the defer block variants, it’s time to explore triggers.
Understanding @defer trigger mechanics
@defer offers two separate points of control, each with its own trigger mechanism:
-
The (optional) prefetch trigger dictates the timing for downloading the bundle from the server.
-
The (optional)
@defertrigger determines when the block's content is actually shown to the user.
Bear in mind that these are two independent operations, allowing for separate timing and enabling a wide range of complex scenarios.
For setting up these triggers, you have two main categories:
-
predefined triggers, which handle the standard, frequently-encountered situations
-
custom triggers, for scenarios where you need more specific control
The on keyword is used to apply a predefined trigger. For custom triggers, you'll use the when keyword.
Let's first look at the complete set of predefined triggers:
- idle
- viewport
- interaction
- hover
- immediate
- timer
Each of these can be used for either loading the bundle or for displaying the deferred content.
We’ll examine each one in detail.
The default behavior of the idle trigger
The idle trigger serves as the default for both prefetching and showing the @defer block.
Consider this @defer block without any triggers specified:
@defer {
<large-component />
}
It’s completely analogous to writing:
@defer (on idle; prefetch on idle) {
<large-component />
}
Take note of the use of the on keyword here, which is proper for a pre-built trigger rather than when.
As we've noted, idle handles both the default prefetching and display.
So what does this mean in practice?
The idle trigger activates once the browser hits an idle state.
This indicates that the browser has finished fetching all page resources and has no other tasks queued up.
Angular determines this by leveraging the standard requestIdleCallback browser API.
When does this happen? In most typical applications, the browser enters an idle state almost immediately after the page loads, once the browser’s loading spinner has stopped.
Given that @defer is part of a template, the idle trigger usually fires right after the component renders for the first time.
The viewport trigger paired with @placeholder
A very popular reason to use @defer is to render a component only when it scrolls into the user's viewport on a longer page.
This tactic lets the page render above-the-fold content first, which is what users see right away.
The remaining content is fetched only when a user actively scrolls down to it.
This not only speeds up the initial load but also prevents unnecessary downloads for off-screen components.
The viewport trigger facilitates this exact scenario:
Example:
@defer (on viewport) {
<large-component />
} @placeholder {
<loading-spinner />
}
In this case, the event is set off when the @placeholder block enters the browser viewport.
This behavior relies on the @placeholder block containing just a single root element.
The viewport trigger without a @placeholder
If you don't have a placeholder defined, you can still target a specific element for the event.
You can achieve this by using a template reference variable:
Example:
<div #title>Title</div>
@defer (on viewport(title)) {
<large-component />
}
The trigger will now fire when the #title element becomes visible within the viewport.
The interaction trigger with @placeholder
This particular trigger responds to direct user interaction with an element on the page.
Interaction here includes a user clicking on the target or, if it's an input element, typing into it.
Under the hood, Angular detects this using click or keydown events.
Example:
@defer (on interaction) {
<large-component />
} @placeholder {
<placeholder-component />
}
Here, the interaction events are monitored on the <placeholder-component />.
The interaction trigger without a @placeholder
Similar to viewport, you can point the interaction trigger to a different element in the page.
A template reference variable is your tool for this.
Example:
<div #title>Title</div>
@defer (on interaction(title)) {
<large-component />
}
The @defer block will only be activated upon interaction with the #title element.
Since #title is a div and not an input, the event will be triggered by a click.
The hover trigger with @placeholder
This trigger activates when a user hovers their cursor over an element. The related events are mouseenter and focusin.
Example:
@defer (on hover) {
<large-component />
} @placeholder {
<loading-spinner />
}
These events fire on the @placeholder block, as long as it consists of a single root element.
The hover trigger without a @placeholder
You have the option to designate a different element to fire the @hover event on hover. Template reference variables allow this.
Example:
<div #title>Title</div>
@defer (on hover(title)) {
<large-component />
}
The @defer block will be triggered once the #title element is hovered over or receives focus.
The immediate trigger explained
As its name implies, this trigger causes the deferred @defer block to load right away, without any event waiting.
Example:
@defer (on immediate) {
<large-component />
}
This means there’s no delay for the browser to become idle to initiate the loading process.
The timer trigger for scheduled loading
This trigger is activated after a set duration expires.
Example:
@defer (on timer(5s)) {
<large-component />
}
The @defer block will load 5 seconds after the component is initialized.
The time value can be defined with either milliseconds (ms) or seconds (s).
This wraps up our review of all the built-in triggers!
Now, let's move our focus to the concept of pre-fetching.
A look at @defer block prefetching
Prefetching is the technique of downloading resources into memory in advance of when they will be required.
In the previous parts, we've shown how each built-in trigger is applied.
Don’t forget, however, that @defer provides us with that second level of control:
- the timing for resource pre-fetching from the server
- the timing for revealing the
@deferblock's content
So far, we have been adjusting the display trigger, leaving the prefetch trigger at its default state.
This configuration example:
@defer (on timer(5s)) {
<large-component />
}
It is functionally the same as this:
@defer (on timer(5s); prefetch on idle) {
<large-component />
}
Observe that the bundle is being pre-fetched as soon as the browser goes idle – the predefined default behavior.
Yet, the <large-component /> is not shown to the user until that 5-second timer runs out, even though the prefetch task completed much earlier.
The idle trigger is a strong default for prefetching, but it's possible to use any of the other predefined triggers, or set up your own custom conditions.
Let's explore a couple of use cases for prefetching.
A use case: viewport prefetch, interaction display
Envision a sizeable component placed below the fold on your page. You want its download to begin only when the user scrolls down.
Following that, you want to display it only when the user interacts with a specific input field.
Here is how you could configure this:
@defer (on interaction; prefetch on viewport) {
<large-component />
} @placeholder {
<input />
}
All the standard triggers work for both prefetching and displaying deferred content.
You can mix them to fit advanced workflows.
However, what if none of the predefined triggers fit your specific requirement?
Building custom @defer triggers using when
When you need more granular control, you can formulate your own triggers with the when expression.
Here's an example:
@Component({
selector: "app",
template: `
<button (click)="onLoad()">Trigger Prefetch</button>
<button (click)="onDisplay()">Trigger Display</button>
@defer(when show; prefetch when load) {
<large-component />
}
`,
})
export class AppComponent {
load: boolean = false;
show: boolean = false;
onLoad() {
this.load = true;
}
onDisplay() {
this.show = true;
}
}
In this case, the prefetch and the display conditions are both based on custom expressions.
This allows you to accommodate unique edge cases perfectly.
The sequence works as follows:
-
Clicking the Preload button starts the download of the
@deferbundle without showing the content. -
The block remains hidden until the
showexpression evaluates to true. -
Clicking the Display button will reveal the
@defercontent, even if its presentation was long after the bundle load finished. -
Now, if you reload and press the Display button first, the prefetch condition is overridden.
-
The clicking of Display will initiate the download and immediately present the content.
The logic here is straightforward: even if your prefetch trigger is custom, the framework will bypass it if the display trigger comes first, performing the fetch on the spot.
This makes sense, as the @defer content can't be rendered before its bundle is available.
How does @defer behave in server-side rendering?
In server-side rendering, browser-specific events are absent from the rendering context.
Consequently, the idle event and timer-based triggers are unavailable, and the viewport event is meaningless without scrolling behavior on the server.
Given these limitations, most @defer triggers are simply disregarded in a server environment.
But then, how does the server treat @defer blocks?
The server avoids eagerly loading or rendering the contents of a @defer block, as doing so would undermine the very purpose of the feature.
The only server-side action related to @defer is rendering the @placeholder block when one is provided.
On the client side, the deferred loading process proceeds as expected once the application has started up.
What is the relationship between @defer and lazy loading?
Consider an application with 20 distinct screens, yet a typical user session only involves 2 or 3 of them.
Why should all 20 screens be loaded upfront?
Such an approach would slow down application startup and degrade the perceived performance.
Moreover, as the application expands, the number of components increases, causing startup time to grow steadily.
Angular addresses this with router-based lazy loading, where components are fetched on demand based on navigation paths.
This way, the code for a particular screen is only loaded when the user navigates to its route.
This mechanism significantly reduces the main bundle size by breaking it into screen-specific chunks, each containing the code for that route.
This router-driven approach is commonly referred to as lazy loading.
But what if the need is to load only portions of a screen, or only certain components within a view?
Consider a screen with numerous components that only appear upon scrolling down, or after a user performs a specific action like submitting a search query.
Why should the code for those components be loaded initially when the user may never need them?
That's exactly where @defer becomes useful.
With @defer, you can load template segments on demand, triggered by logical conditions.
Possible triggers include user actions such as scrolling or clicking a button.
Unlike lazy loading, @defer operates independently of the router, offering a more granular level of control.
It's important to note that @defer doesn't replace lazy loading; rather, it works alongside it.
These features are meant to be combined for optimal performance.
What types of code can be deferred with @defer?
Only standalone components and their associated dependencies—like directives, pipes, and styles—can be loaded via @defer.
This makes it another compelling reason to transition to standalone components, a process that can be automated with the Angular CLI.
How does @defer differ from @if?
These two constructs serve distinct purposes, and @defer should not be viewed as a substitute for @if.
While both can initially hide a block based on a condition, the resemblance ends there.
With @defer, once a condition is met and the component is loaded and rendered, the process is irreversible.
There's no mechanism in @defer to hide the component again if needed; that functionality belongs exclusively to @if.
If you need to show or hide a component conditionally while also deferring its initial load, the two directives can be used together:
@defer (on interaction; prefetch on viewport) {
@if (someCondition) {
<large-component />
}
@placeholder {
<placeholder-component />
}
}
In this arrangement, <large-component /> is loaded only when the user scrolls down.
However, the @defer block being applied doesn't automatically make the component visible.
The <large-component /> is shown only when someCondition evaluates to true; otherwise, it stays hidden.
Ultimately, @defer governs when code is loaded and instantiated, while @if controls when the component appears in the view.
If you found this post informative and want to know when similar articles are published, consider subscribing to our newsletter:
You'll also receive timely updates about the Angular ecosystem.
For a comprehensive exploration of Angular Core features including @defer, check out the Angular Core Deep Dive Course:
Summary
We hope this exploration of Angular's @defer syntax has been valuable.
Clearly, @defer serves as a robust performance enhancement that works in tandem with lazy loading.
These two techniques are designed to be used together, not as alternatives.
What's crucial to grasp is that @defer offers two distinct levels of control that can be configured independently:
-
the timing of the deferred JavaScript bundle's loading is controlled through the
prefetchtrigger -
once loaded, the timing of when the deferred block is applied to the page is managed separately
For both stages, there's a range of predefined triggers at your disposal, and custom triggers can be created when necessary.
In most scenarios, the predefined triggers will suffice.
Should the need arise, though, you can craft fully custom triggers for complete control.
So, what's your take on the new @defer syntax?
We'd love to hear how you plan to leverage it—feel free to share in the comments.
Also, if you have any queries or feedback, don't hesitate to reach out; we're here to assist.
