One of the most effective methods for improving the performance of a single-page application is to defer the loading of non-critical resources as much as possible.

Critical resources should be loaded first, while other less crucial resources should be loaded later, only when and if needed.

There is a good chance that a large portion of your code base is not needed for a typical user session.

So why load code that we are not 100% sure we will need? Loading unnecessary code just slows down our application.

Angular has had so far one major built-in mechanism for avoiding loading unnecessary resources: router-based lazy loading.

This mechanism is very powerful, but it has a limitation: it doesn't allow to lazy-load parts of a given template.

But what if you have a large screen with lots of dependencies, and you want to load parts of the page that are not immediately visible in the background, while you wait for the user to scroll down the page, or to click on a button?

This type of very fine-grained deferred loading is just not possible with router-based lazy loading.

But that is exactly where @defer comes in!

@defer allows all of those scenarios and much more.

In this guide, we will explore how to use the @defer template syntax to take the performance of your Angular applications to the next level.

We will also explain its relation to lazy loading, and how it compares to it.

Table of Contents

This post covers the following topics:

  • What is @defer, and why do we need it?
  • @defer in action
  • @defer with @placeholder
  • @placeholder parameters
  • @defer with @loading
  • The difference between @placeholder and @loading
  • @defer with @error
  • How do @defer triggers work?
  • The idle built-in trigger
  • The viewport built-in trigger with @placeholder
  • The viewport built-in trigger without @placeholder
  • The interaction built-in trigger with @placeholder
  • The interaction built-in trigger without @placeholder
  • The hover built-in trigger with@placeholder
  • The hover built-in trigger without @placeholder
  • The immediate built-in trigger
  • The timer built-in trigger
  • Prefetching @defer blocks
  • Prefetching on the viewport, displaying on interaction
  • Custom @defer triggers with the when keyword
  • What happens when @defer is used with server-side rendering?
  • How does @defer compare to lazy loading?
  • What can be loaded using @defer?
  • How does @defer compare to @if?
  • Summary

Note: If you are looking to learn about what else was introduced in Angular 17 together with @defer, check out the following posts:

Read more on @defer in the official docs: Angular @defer.

What is @defer, and why do we need it?

@defer is an Angular template syntax, that allows you to load parts of a template only when they are needed, given a logical condition.

The @defer syntax allows us to implement common use cases such as:

  • only load a large component after the user scrolls down the page after a certain point
  • only load a large component after the user clicks on a button
  • preload a large component on the background while the user is reading the page, so that it is ready by the time the user clicks on a button

And these are just a few common examples of what we can do with @defer.

The @defer syntax gives us two levels of control over the process of loading partial resources:

  • we can control when the code gets fetched from the backend, and this is called prefetching.

  • we can also control separately when that code is applied to the page

It's important to understand that these two steps are not the same thing.

You might want to load some code on the background anticipating that it will be needed soon, but you don't have to display that code immediately after it gets loaded.

You might want to wait for a certain event to happen, like the user clicking on a button, before you display the code.

With @defer we can define triggers for each of these two steps separately.

We can use predefined triggers, but we can also define our custom triggers if needed.

So we have full flexibility over the whole process of loading and applying the code.

In general, the predefined triggers will be more than enough for most use cases.

Let's then see @defer in action!

@defer in action

Imagine that your component has some code that will always be displayed, but then there is a section of the page that is only needed in certain scenarios.

That non-essential section contains a large component, that we want to load on the background.

Here is a simple example of that component:

@Component({
  selector: "large-component",
  template: ` <h2>large-component is displayed...</h2> `,
})
export class ExampleComponent {}

Just imagine that this is a massive component, that internally uses chart libraries and other heavy dependencies.

We want to delay the loading of this large component until we are 100% sure that the user needs it.

To control when that large component gets loaded in our application, start by wrapping it in a @defer block:

@Component({
  selector: "app",
  template: `
    <h2>Some content that will always be displayed ...</h2>

    @defer {
    <large-component />
    }
  `,
})
export class AppComponent {}

This will tell Angular that the @defer section of the page should be packaged into a separate Javascript bundle, and loaded separately.

You can see the immediate effect of the presence of the @defer block in the Angular CLI build output:

Angular CLI build output where the extra bundle created by @defer is visible

As you can see, the use of @defer has created an extra bundle, that contains the code for the large-component element.

This means that large-component and all its dependencies have been extracted from the main application bundle, and are now part of a separate bundle.

So the main application bundle now is smaller, and it should load faster!

The new extra bundle with large-component will be loaded only when the @defer block gets triggered.

Notice that we didn't specify any trigger for the @defer block, so it will be triggered by default when the browser is idle.

The browser is considered idle when all the resources of the page have finished loading, and the browser loading spinner has stopped.

This means that the large-component bundle will be loaded and displayed very soon after the AppComponent is fully rendered.

To make it clearer, this is what is happening at this point:

a bundle getting loaded with @defer

As we can see, the following is happening:

  • the main application bundle is loaded first
  • then the @defer bundle is loaded, containing the large-component and all its dependencies
  • we can see the extra bundle being loaded last in the network tab, at the bottom right
  • then the large-component is rendered, as we can see on the top right

So this establishes the fundamentals of how @defer works under the hood.

But now the question is, how can we better control the behavior of @defer:

  • how can we control when the bundle gets loaded?
  • how can we control when the large-component gets rendered?
  • can we display a loading indicator while the bundle gets loaded?
  • can we display an error message if the bundle fails to load?
  • can we display some initial content that will be displayed before the bundle gets loaded?

We are going to answer all these questions next!

But to answer many of these questions, we need to first introduce the companion blocks of @defer: the @placeholder, @loading, and @error blocks.

@defer with @placeholder

Sometimes we just want to display a blank space in the place where our
@defer block is placed.

But other times, we want to display some initial content to the user, that will later be replaced by the code loaded via the @defer block.

We can do that by wrapping the initial content that we want to display in a @placeholder block:

@defer {
  <large-component />
} 
@placeholder {
  <initial-content />
}

So here is how this works:

  • Initially, the <initial-content /> component is rendered, in the place where the @defer block is placed inside the page.

  • When the @defer block gets loaded and rendered, only then will the <initial-content /> be replaced with <large-component />.

Note: Any components, directives, pipes, etc. used inside the @placeholder will be eagerly loaded, and will be part of the main bundle.

So do not use heavy dependencies that you want to be deffer loaded inside the @placeholder block.

The placeholder component should typically be something very simple and lightweight.

It's just a visual indicator that something will be placed in that space soon.

@placeholder parameters

The @placeholder block can take one optional parameter called minimum, which is a time duration in seconds or milliseconds.

minimum is used to set the minimum amount of time for which the placeholder block will be shown to the user:

Example:

@defer {
  <large-component />
} 
@placeholder (minimum 2s) {
  <initial-content />
}

The above code will render the <initial-content /> for 2 seconds before the <large-component /> appears.

This minimum display time is useful to help prevent bad UI user experience.

This is because depending on the network speed, the @defer block might show the placeholder just for a split second.

In that case, the user would just see some weird flickering effect on the page, which could give the feeling that something is broken, when it's not the case.

The minimum parameter helps with that, as it makes sure that the placeholder is displayed long enough for the user to see it and understand what it is, before it disappears.

@defer with @loading

The @loading block is used to show some content while the @defer block is still loading its Javascript bundle in the background.

Example:

@defer {
  <large-component />
} 
@loading {
  <loading-spinner />
}

The <loading-spinner /> will be rendered only while the <large-component /> bundle loads. When the loading is completed, the <loading-spinner /> will be removed from the page, and the <large-component /> will be rendered in its place.

Notice that any dependencies inside the @loading block are all eagerly loaded as well, just like in the case of @placeholder.

The @loading block accepts two optional parameters:minimum and after:

  • minimum is used to specify the minimum amount of time that the @loading block will be shown to the user.

  • after is used to specify the amount of time we should wait before showing the @loading indicator after the loading process is started.

Both parameters can be expressed in seconds or milliseconds, and they both are meant to help unwanted flickering effects in the user interface.

Here is an example of how these parameters can be combined:

@defer {
<large-component />
} @loading (after 1s; minimum 2s) {
<loading-spinner />
}
  • The @loading block above will wait for 1 second after the loading begins before displaying its block contents.
  • The @loading block contents will be displayed for 2 secs before the <large-component /> is rendered.

Notice that if the loading takes less than the after value of 1 second, then the @loading element will never be displayed.

This means that:

  • the loading indicator will only be shown if the loading takes more than 1 second, otherwise, it will never be displayed

  • if the loading indicator ever gets shown and then the loading completes, we don't want it to be hidden from the user too quickly.

  • so we set a minimum display time of 2 seconds. This means that if the loading indicator ever gets shown at all, it will be displayed for at least 2 seconds.

This prevents the indicator from disappearing too quickly, which could give the impression that something is broken.

The difference between @placeholder and @loading

The two blocks seem similar, but they serve different purposes.

The @placeholder is displayed initially until the contents of the @defer block are ready to be rendered.

This block is displayed even before the bundle loading starts. Remember, maybe the loading is triggered only after the user clicks on a button.

So until the bundle loading gets triggered, we might want to display some content to the user and that is where @placeholder comes in.

The @loading block on the other hand only displays when the loading of the bundle of the @defer block has started, and is still ongoing.

After the loading is completed, it disappears.

@defer with @error

But what if something goes wrong while the bundle is loading, like a network error?

The @error block is used to display content when the loading of the @defer block fails for some reason.

Example:

@defer {
<large-component />
} @error {
<error-message />
}

So now that we are familiar with the multiple defer blocks, let's talk about triggers.

How do @defer triggers work?

@defer has two levels of control, each with its trigger:

  • the optional prefetch trigger, which controls when the bundle gets loaded from the backend

  • the optional @defer trigger, which controls when the @defer block gets displayed to the user

Remember, those are two very different events, and we can control them separately and support all sorts of advanced use cases.

When it comes to choosing the right trigger, we have two options available:

  • we can use predefined triggers, that cover all the most common use cases

  • but we can also define our custom triggers, if necessary

The keyword on is used for predefined triggers, while the keyword when is used for custom triggers.

Let's start with the predefined triggers, here is the complete list:

  • idle
  • viewport
  • interaction
  • hover
  • immediate
  • timer

These predefined triggers can all be applied both as prefetch triggers or as @defer display triggers.

Let's go through them one by one.

The idle built-in trigger

This is the default trigger for @defer, both for prefetching and for displaying the @defer block.

So let's say that we use @defer without any trigger:

@defer {
<large-component />
}

This is equivalent to:

@defer (on idle; prefetch on idle) {
<large-component />
}

Notice that we are using the on keyword and not when, because this is a predefined trigger.

As you can see, idle is both the default prefetch trigger and the default display trigger.

So how does it work?

idle triggers when the browser enters the idle state.

This idle state occurs when the browser is done with loading all page resources and is not busy with any other task.

Angular detects this by using the standard requestIdleCallback browser API.

When will this happen? For most applications, the browser will be idle immediately after the page is loaded when the browser loading spinner stops running.

As @defer is part of a component template, this means that idle will usually get triggered in most cases immediately after the component is rendered for the first time.

The viewport built-in trigger with @placeholder

A very common use case for @defer is to want to load a component only when (and if) it becomes visible on the browser's viewport, after scrolling down a long page.

With this strategy, the page will render first the content above the fold, which is immediately visible to the user once the page is loaded.

The rest of the content will be loaded only when and if the user scrolls down the page.

This way, the initial page will load faster, and we avoid loading code that the user might never see.

To support this use case, we have available the viewport trigger:

Example:

@defer (on viewport) {
<large-component />
} @placeholder {
<loading-spinner />
}

In this case, the viewport event will be triggered when the @placeholder block becomes visible in the browser viewport.

For this to work, the @placeholder block must contain only a single node.

The viewport built-in trigger without @placeholder

If we don't have a placeholder available, we can still specify an element that triggers the viewport event when it comes into view.

This is done using a template variables:

Example:

<div #title>Title</div>

@defer (on viewport(title)) {
<large-component />
}

This way, the trigger will fire when the #title element becomes visible in the viewport.

The interaction built-in trigger with @placeholder

This trigger is fired when the user directly interacts with an element of the page.

The interaction happens either when the user clicks on the target element, or when it types something in it, in case the element is an input.

An interaction is detected using click or keydown events.

Example:

@defer (on interaction) {
<large-component />
} @placeholder {
<placeholder-component />
}

In this case, the interaction events are detected in the <placeholder-component />.

The interaction built-in trigger without @placeholder

Just like with viewport, we can specify an alternative element in the page that will trigger the interaction event.

This is done using a template variable.

Example:

<div #title>Title</div>

@defer (on interaction(title)) {
<large-component />
}

The @defer block will be triggered only when the #title element is interacted with.

As #title is a div and not an input, this means that the click event will be used to detect the interaction.

The hover built-in trigger with@placeholder

This trigger is fired when the user hovers over an element of the page. The hover events are mouseenter and focusin.

Example:

@defer (on hover) {
<large-component />
} @placeholder {
<loading-spinner />
}

These events are triggered on the @placeholder block, provided that it contains only a single node.

The hover built-in trigger without@placeholder

We can also specify an element that will trigger the @hover event when it is hovered over. This is done using template variables.

Example:

<div #title>Title</div>

@defer (on hover(title)) {
<large-component />
}

The @defer block will be triggered when the #title is hovered over or focused.

The immediate built-in trigger

This triggers the @defer block immediately. It does not wait for any event to be fired.

Example:

@defer (on immediate) {
<large-component />
}

This means that we won't even wait for the browser to be idle to trigger this event.

The timer built-in trigger

This trigger is fired when a timer duration is reached.

Example:

@defer (on timer(5s)) {
<large-component />
}

The @defer block will be triggered after 5 seconds.

The value of the timer can be in milliseconds (ms) or seconds (s).

And with this, we have covered all the built-in triggers!

Let's now talk about pre-fetching.

Prefetching @defer blocks

Prefetching is the process of loading resources and keeping them in memory before they are needed.

In the previous sections, we demonstrated on each built-in trigger works.

But remember, when using @defer we have two levels of control:

  • we can control when the bundle gets pre-fetched from the backend
  • we can also control when the @defer block gets displayed to the user

So far we have only been configuring the display trigger while leaving the prefetch trigger in its default state.

So this configuration:

@defer (on timer(5s)) {
<large-component />
}

Is functionally equivalent to:

@defer (on timer(5s); prefetch on idle) {
<large-component />
}

As you can see, the bundle is getting pre-fetched when the browser becomes idle, which is the default behavior.

But then the <large-component /> is only shown to the user after 5 seconds, even if the pre-fetching has finished long before that.

The idle trigger is a great default for prefetching, but we can also use any of the other built-in triggers that we have learned, as well as define our custom triggers.

Let's see a couple of examples of what we can do with the prefetch option.

Prefetching on the viewport, displaying on interaction

Imagine that you have a large component below the fold, so you want to start loading it only when the user scrolls down the page.

Then you want to display it only when the user types in something on an input box.

You can do that in the following way:

@defer (on interaction; prefetch on viewport) {
<large-component />
} @placeholder {
<input />
}

As you can see, all the built-in triggers can be used both for prefetching and for displaying the @defer block.

We can combine them to support all sorts of advanced use cases.

But what if the existing built-in triggers don't do exactly what we need?

Custom @defer triggers with the when keyword

If we need to, we can define our custom triggers using the when keyword.

Here is 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 example, both the prefetch and the display events are configured using custom triggers.

This gives us full flexibility to implement any custom edge cases that we might run into.

Here is how this works:

  • click on the Preload button, and the loading of the @defer Javascript bundle will be triggered in the background

  • However, the @defer block will not be displayed yet, because the show variable is still false

  • Then click on the Display button, and the @defer block will be displayed, long after the bundle has finished loading

  • On the other hand, refresh the page, and this time click on the Display button first.

  • You will notice that this will trigger the prefetching as well, and then the block gets immediately displayed.

This means that we can configure a custom prefetch trigger, but if the display trigger gets fired first, the pre-fetching will be done instantly and the prefetch condition will be bypassed.

This makes sense because we can't display the @defer block without the bundle being loaded first.

What happens when @defer is used with server-side rendering?

When an application is rendered on the server, all the typical browser events are not available in that context.

This means the idle event is not available, nor is the timer event, and the viewport event does not make sense on the server as there is no notion of scrolling, etc.

This means that most of the @defer triggers do not make sense in a server context, so they are all ignored.

So, how can the server handle @defer blocks then?

The server doesn't want to eagerly load the content of the @defer block and render it either, because that defeats the whole purpose of the @defer feature.

The only thing that the server can do when it comes to @defer is to do the initial render of the @placeholder block, if it exists.

Then on the front end, the deferred loading will take place as usual, after the application startup.

How does @defer compare to lazy loading?

Imagine an application with 20 different screens, but for typical user sessions, only 2 or 3 are usually needed.

Why load all 20 screens at once then?

That delays the startup of the application and makes it feel slower.

Worse, as the application grows, the number of screens and components grows as well, and the application startup time will keep increasing over time.

To help with that, Angular provides us with lazy loading of components, based on routing configuration.

So if a user navigates to a certain browser path, only then the code for that screen will be loaded.

This already helps a lot in reducing the size of our main application bundle, by splitting it up into screen-level bundles that contain all the code for a given screen.

This router-based mechanism is known as lazy loading.

But what if you want to load only parts of a screen or only parts of a component?

Imagine that one of your screens has a ton of components, that are only visible if the user scrolls down the page, or when the user types in a search bar and clicks enter.

Why load the code for all those components initially, if the user might not even need them?

That is where @defer comes in.

The @defer syntax allows you to load parts of a template only when necessary, given a logical condition.

This deferred loading can be triggered by a variety of events, like the user scrolling down the page, or the user clicking on a button.

The @defer mechanism is not linked to the router, unlike lazy loading, and it's much more fine-grained, we have a lot more control over it.

The @defer feature does not replace lazy loading, it complements it.

The two features are designed to be used together.

What can be loaded using @defer?

We can only use @defer to load standalone components and their dependencies: components, directives, pipes, CSS, etc.

So one more good reason to migrate our applications to standalone components, using the automated migration available via the Angular CLI.

How does @defer compare to @if?

The two serve two completely different purposes. @defer is not a replacement or an alternative to @if.

It's true that @defer will also initially hide a block given a logical condition just like @if, but that is where the similarities end.

With @defer, once the logical condition gets triggered and the component gets loaded and displayed, there is no turning back.

There is no way to hide the component again if needed using @defer, that can only be done with @if.

So if you need to conditionally show or hide a component that you don't want to load initially, combine the two:

@defer (on interaction; prefetch on viewport) { 
  @if (someCondition) {
    <large-component />
  } 
  @placeholder {
    <placeholder-component />
  }
}

With this setup, the <large-component /> will only be loaded when the user scrolls down the page.

The @defer block will then be applied to the page, but that does not mean that the <large-component /> is visible yet.

The <large-component /> will only be displayed if someCondition is true, if the condition is false, it will remain hidden.

The bottom line is that @defer is meant to control when the code gets loaded and applied to the component, and @if is meant to control the component visibility.

I hope that you enjoyed this post, to get notified when new similar posts like this are out, I invite you to subscribe to our newsletter:

You will also get up-to-date news on the Angular ecosystem.

And if you want a deep dive into all the features of Angular Core like @defer and others, have a look at the Angular Core Deep Dive Course:

Summary

I hope you enjoyed this deep dive into the Angular @defer template syntax.

As you can see, @defer is a powerful performance optimization tool that is complementary to lazy loading.

The two are meant to be used together, one does not replace the other.

The key thing to understand about @defer is that it gives us two separate levels of control, that we can configure separately:

  • we can control when the deferred Javascript bundle gets loaded with the prefetch trigger

  • once the bundle is loaded, we can then control when the deferred block gets applied to the page

To control both of these steps, we have a variety of predefined triggers available, and we can also even define our custom triggers if we need to.

In general, the predefined triggers should be more than enough for most use cases.

But if we really need to, we have full flexibility for creating our own custom triggers.

So, what do you think about the new @defer syntax?

Let me know in the comments below how you are planning on using it.

If you have any questions or comments, let me know as well. I'll be happy to help!