Total guide to lazy loading with Angular @defer

Learn everything about the lazy loading of standalone components with Angular @defer block including best practices, deep dive on all interactions and gotchas including a live demo!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

14.11.2023

13 min read

Total guide to lazy loading with Angular @defer
share

What's the best way to procrastinate preparing of an Angular Signals talk for the upcoming Angular Zurich Meetup? (make sure to join us if you're form the region😉)

Crushing enemies as a paladin in Baldur's Gate 3 is definitely a strong contender but taking a deep dive on the new Angular @defer sounds even better, so let's go!

Angular 17 is here, and it has arrived with a very strong line up of amazing goodies!

The new @defer block, which allows us to lazy load Angular standalone components, is by far the most exciting and impactful feature of Angular 17 (at least in my books)!

Angular @defer example (screen recording) Angular @defer example (screen recording)

💡 With the @defer being released, I would like to point out that the Angular standalone components are now objectively better solution for every possible use-case in Angular applications.

Previously I was on the fence about the trade-off between their usefulness and the necessary efforts, especially for implementing (and migrating) of components in the lazy loaded feature modules in existing applications!

Migrating all your components to standalone makes them " @defer ready " which is great because it will become trivial to lazy load them once they grow to a meaningful size!


The Angular @defer at glance

The @defer syntax allows us to lazy load any Angular standalone component with exceptionally great DX and API which covers almost every use case that you can imagine!

As with every new thing, we have to develop new mental models to learn how to use @defer correctly and efficiently

🏺 Quick historical recap; Angular had APIs that allowed us to lazy load components already back in the days of Angular 5, but the APIs were everything but developer friendly.

This got better with the advent of IVY in Angular 9 and even better in Angular 14 with standalone components.

Nevertheless, the resulting code was still very low-level, verbose and needed a lots of custom logic to handle basics like placeholders, loading and error states!

Now let's have a look on the @defer itself…
The most basic usage of @defer is to wrap a component in the template inside a @defer block…

@Component({
  selector: 'my-org-parent',
  standalone: true,
  imports: [HeavyComponent],
  template: `
    @defer {
      <my-org-heavy />
    }
  `,
})
export class ParentComponent {}

The @defer is available in the template out of the box, no need to import anything!

This will instruct Angular compiler to extract the HeavyComponent into its own JavaScript bundle file which will then be loaded once the ParentComponent template is rendered. This could be easily verified and seen in the network tab of the dev tools of your favorite Chrome, ehm I mean browser.

The example above works, but is by no means realistic, because it's missing out on all the additional available features which we're going to explore soon!

Let's compare this with a realistic example which does much more and will lead to much better user experience!

@Component({
  selector: 'my-org-dashboard-item',
  standalone: true,
  imports: [ChartComponent],
  template: `
    @defer (on viewport; prefetch on timer(2000))  {
      <my-org-chart />
    } @placeholder {
      <my-org-skeleton type="chart" />
    } @loading {
      <my-org-skeleton type="chart" [animate]="true" />
    } @error {
      <my-org-error-feedback />
    }
  `,
})
export class DashboardItemComponent {}
An example UI representation of the Angular @defer example above (including the states over time) An example UI representation of the Angular @defer example above (including the states over time)

I hope this spiked your interest so let's learn about all the things that are at our disposal!

Prerequisites

  • component must be marked as standalone: true and should be located in its own dedicated file, and the only "thing" in that file, eg no tokens, consts, functions, ... which could be eagerly imported elsewhere, as that will break the lazy-loading
  • component can only be used in the parent template (so not in @ViewChild, …)
  • components, directives and pipes used in the template of the deferred component can be both standalone and NgModule based, but always think about overall dependency graph, to prevent situation when "every" component depends on "every" other component!

The Angular @defer API

First, I think it will the best to zoom out and see the full picture before we dive right back into examples and best practices! That way we will have full overview of what's possible.

The APIs itself is very powerful and allows us to express many different setups with basically just 2 main systems:

  • trigger — defines when and how the component should be lazy loaded
  • prefetch — defines if, when and how the component lazy bundle should be pre-fetched

Knowing this, we can say that defer is defined as combination of 0 to many trigger and prefetch expressions, when multiple expression of the same type are present, they are joined with the logical OR operator, eg trigger1 OR trigger2

@defer (trigger1; trigger2; ... prefetch1; prefetch2; ...) { }

The @defer triggers

There are two types of @defer triggers:

  • on (declarative) — uses one of the available behaviors (see bellow)
  • when (imperative) — uses any custom logic that returns true or false (eg component property or method, Signal, RxJs stream, …

Declarative "on" triggers

Let's explore available declarative @defer (on <trigger>) {} triggers which will be sorted from the most eager to the most lazy (and or custom)…

  • immediate — the component lazy loading is triggered immediately during the parent component template execution
  • idle — (default) Angular will lazy load the component on the first available requestIdleCallback (browser API) which allows us to perform background and low priority work on the main event loop
  • timer(delay) — after a specified delay
  • viewport (target) — when the @placeholder (described below) or optional target are in the viewport (detected using browser IntersectionObserver API)
  • hover (target?) — when the @placeholder or optional target are hovered by the user, for this purpose Angular considers the mouseenter and focusin DOM events
  • interaction (target?) — when the @placeholder or optional target are interacted with by the user, for this purpose Angular considers the click and keydown DOM events

⚠️ Duplicate on handlers of the same type are not allowed! For example, on hover (placeholder) together with on hover(someTarget) can't be used together on a single @defer block! Currently, there can be only a single target passed into the triggers which support them.

Imperative "when" triggers

The declarative on triggers should cover most of the basic use-cases that will be useful in our applications but Angular allows us to customize it even further!

The imperative @defer (when <customTrigger>) {} can cover literary any use case which we can imagine (and implement)!

The custom trigger can be any property, method, Signal or RxJs stream, and it should evaluate to a Boolean flag. It will trigger lazy loading of the component once resolved to true and changing it back to false won't have any further impact so the when trigger represents a one way road!

In practice, we will use when based custom triggers to lazy load components in a "programmatic way" as a…

  • outcome of some async processing — show success page (or next step) once the form was submitted successfully
  • step in some process — load the next step once the previous step was finalized
  • outcome of a calculation — show additional info if the value of the offer reached some threshold
  • and many other — let me know in the comments what are the use cases which you think will be a great fit for the when trigger and I will add them to this list 😉
@Component({
  selector: 'my-org-process-container',
  standalone: true,
  imports: [Step1Component, Step2Component /* ... */],
  template: `
    <!-- other steps... -->
    @defer (when process.step1.finished) { // signal() or rxjsStream$|async
      <my-org-step-2 />
    }
  `,
})
export class ProcessContainerComponent {}

Default trigger

Remember our first example at the beginning of this article?

@defer {
  <my-org-heavy />
}

The @defer without any trigger will use the on idle by default.

Multiple components in a single @defer block

Until now, we only spoke about the case with a single Angular standalone component wrapped in the @defer block but what about the following case?

@defer (on viewport)  {
  <my-org-bar-chart />
  <my-org-line-chart />
  <!-- ... -->
}

Angular supports multiple standalone components inside of a single @defer block!
Building (or serving) such application will yield terminal output similar to the…

Lazy Chunk Files    | Names                | Raw Size   |
chunk-AOHCSC3Y.js   | bar-chart-component  |  165.25 kB |
chunk-C5XGGIZ2.js   | line-chart-component |   84.20 kB |

And these chunks will be lazy loaded simultaneously when the @defer block is triggered!


Follow me on Twitter (X) because that way you will never miss new Angular, NgRx, RxJs and NX blog posts, news and other cool frontend stuff!😉


The @placeholder, @loading & @error blocks

Until now, we have only seen how to use the @defer itself but already mentioned the @placeholder which plays an important role for some of the declarative on triggers as a default target (when no target was specified).

⚠️ Keep in mind that any Angular components which we will use as the content of these three blocks will be eagerly loaded compared to the components in the @defer block which is lazy loaded!

The @placeholder

The @placeholder allows us to specify what should be rendered before the loading of the lazy component was initiated.

As such, placeholder represents a natural fit for implementation of something simple as empty <div> that reserves the necessary space all the way to the fancy "skeleton UI" version of the lazy loaded component to provide best possible user experience!

The @placeholder block supports specifying of the minimum option which represents the minimum duration for which the content of the @placeholder block will* be displayed (* as it turns out, there are some non-obvious interactions which we're going to list a bit later)

The @placehodler block is the second most important block and we should use it ALWAYS when using the @defer block itself!

The @loading

The @loading block is another optional block that can be used in conjunction with the @defer. The loading block will replace the @placeholder block in the view during the loading (in practice usually just a short time) of the lazy loaded standalone component JavaScript bundle file.

This sounds pretty straight forward, but soon we will see that it's not always the case!

The @loading block also supports the minimum option (the minimum amount of time the content of the @loading block will be displayed) and additionally also the after option which represents the "minimum" amount of time that the loading process has to take to show the @loading block at all…

For example, let's say that the <my-org-chart /> can be loaded faster than the 100ms, then the content of the @loading block will not be displayed at all!

@defer (on viewport) {
  <my-org-chart />
} @placeholder {
  <my-org-skeleton type="chart" />
} @loading(after: 100ms; minimum: 500ms) {
  <my-org-skeleton type="chart" [animate]="true" />
}

The @placeholder & @loading interactions in practice

  • @placeholder with minimum + fast loading time = we won't see the @loading block
  • @loading with after + fast loading times = we won't see the @loading block
  • on immediate + @loading block = we won't see the @placeholder block

The @error block

The @error block allows us to specify what should be displayed in case that the loading of the lazy loaded standalone component JavaScript bundle file has failed.

@defer {
  <my-org-heavy />
} @error {
  <p>Loading of the component failed...</p>
}

As of version 17, there is no extra "error description" available to parametrize the error message nor any retry mechanism, but let's hope and upvote the issue so that the capabilities will be extended in the future! 🤞

Prefetching

Last but not least, let's talk about the prefetch statements which belong to the @defer block… (remember, multiple statements are joined by the logical OR operator)

@defer (trigger1; trigger2; ... prefetch1; prefetch2; ...) { }

Similar to the triggers there are two types of prefetch triggers

  • on (declarative) —uses one of the available behaviors (see bellow)
  • when (imperative) — uses any custom logic that returns true or false (eg component property or method, Signal, RxJs stream, …

The prefetch on supports the same triggers as on, let's have a look on when it would make most sense to use them…

  • immediate — use idle instead
  • idle — good candidate for many use cases
  • timer(delay) — use instead of idle if you expect that there is going to be some work to be done "early", eg we navigate to a feature and then need to process lots of data once it's loaded from backend, the idle might get in a way of that, so we might want to postpone prefetching to a "safer" time after that
  • viewport (target) — makes sense to use with target which appears before the @defer on the same page as then it's better to just do a standard on viewport trigger instead of the prefetch
  • hover (target?) — makes sense to use with target which appears before the @defer on the same page as then it's better to just do a standard trigger instead of the prefetch
  • interaction (target?) — makes sense to use with target which appears before the @defer on the same page as then it's better to just do a standard trigger instead of the prefetch

The prefetch when can be a great way to implement custom behavior where we prefetch the component bundle on scroll, eg when its getting "close" to the viewport (so before that).

The prefetch and @loading block interactions

The @loading block won't be displayed if the lazy component bundle was already prefetched!


Best practices

As we have seen, interaction between different blocks and their options gets quite complex, especially when combined with the real world network conditions. For these reasons, I would strongly suggest to always try to…

Make @placeholder and @loading blocks look the same in the form of skeleton UI components

That way, when the @loading block is displayed, it will look exactly the same as the placeholder and only add some minor animation which greatly reduces visual noise and will lead to better user experience! Example of this can be seen in the live demo bellow!

This is in stark contrast to noisy and probably "jumpy" UI which could switch between the placeholder (skeleton), loading (spinner) and actual component (chart) within one second and each of them would probably be of a different size!

A simpler version of this would be to only use @placeholder block and skip the @loading block altogether!

Don't specify minimum for the @placeholder blocks

This might feel "bad", especially if we invested some time and have this amazing looking skeleton placeholder with amazing animation, but we should always focus on the user! Let the user see real component as soon as possible! This again works the best if placeholder, loading and real component look as similar as possible!

How to determine what to lazy load?

The @defer is great and easy to use so should we now start lazy loading every single component in our application!?

The answer is, NO!

The best way to think about this is looking at the two extremes of the spectrum. Let's imagine a lazy loaded feature (page) with 10 components which are nested in each other, eg container -> list -> item -> form -> rich-editor -> controls -> dropdown -> option -> …

  1. Every component is lazy loaded — we will need to load previous one before we know what to load next which would lead to a waterfall of requests! This is definitely suboptimal and leads to slower time to load the whole page and worse UX!
  2. Every component is eagerly loaded — a standard dependency in the imports: [] of parent Angular standalone component, well this works great until one of those components becomes excessively "heavy" (eg chart, editor, …) In that case it will become beneficial to split that one heavy component away from the main lazy loaded feature bundle to prevent delaying of the whole feature!

The @defer vs lazy routes

The @defer represents a new very easy to use way to lazy load standalone components, which naturally raises a question...

Does new @defer make the original Angular Router based lazy loading obsolete?!

And the answer is resounding, NO!

Angular routing represents a fundamental building block of any non-trivial Angular application.
Routes represent a great way to split application into multiple independent parts which has positive impact on overall architecture and therefore maintainability and extendability of the application!

All this could be in theory possible to achieve using the @defer and some conventions, but...

Routing allows us to receive state from the URL (and reflect it back) of the current page, which is crucial for implementation of features like eg SEO, sharing, deep-linking, bookmarking, …

Because of this, @defer represents a complementary Angular mechanism to lazy load standalone components within a single feature (page) which is lazy loaded using the Angular Router!

Many thanks to Deborah Kurata for the feedback on this section! 🙏

The "on timer" vs "prefetch on timer"

Comparing the on timer(delay) and prefetch on timer(delay) will allow us to explore another very interesting tradeoff which we can make when deciding what is the best behavior for our use case!

It boils down to how expensive is to "run" the lazy loaded component, for example when it triggers additional backend request or processing:

  1. if the component execution is "expensive" then we might want to go with prefetch as that would lazy load the bundle, but it won't "run the component" until some other condition, eg on viewport
  2. if the component execution is "cheap" then we might want to lazy load and "run it" (eg with on timer(delay)) even if it's not visible on the screen

⚠️ If you want to use on timer(delay) always make sure you use it in conjunction with something like on viewport or on hover as user might reach component faster than the specified delay and using only the on timer would block loading of that component until the specified delay has passed!

Does it ever make sense to use on immediate (or idle) triggers?

The on immediate and on idle triggers start to load the lazy component almost instantly once the user navigates to a given page with the parent component who uses @defer in its template.

This might seem like a "waste" as what is the point of lazy loading something if we lazy load it immediately? We could have just included it in the standard way reduce amount of the requests, right?

Such approach still can make sense if we want to make sure the "main part" of the page (feature) is loaded and displayed before we start loading some heavy component like a chart (which could easily represent more than 1/2 of the total payload of the lazy feature)!

Remember, it might still make sense to further optimize it with more specific on triggers!

Prefetch

The prefetch on viewport makes little sense as if we prefetch it and, it's already in the viewport, then we should display it, and therefore it makes much more sense to use the on viewport trigger instead!

On the other hand, having a custom prefetch when customScrollBasedAlmostInViewport would make much more sense!

Maybe this would be something to consider for the Angular team to add to the prefetch on viewport(offset: 200) as an option? This would start prefetching 200px before the target reaches the viewport? If you find this use case interesting make sure to upvote the issue in Angular GitHub repository! 👍

Live demo and @defer playground

Make sure to put what you just learned in practice in this live demo!

Angular @defer live demo in StackBlitz

Testing

Fun fact; Researching testing for this article led to discovery of a "small bug" in the official Angular docs and a subsequent PR 💪

Angular got our back also for testing of the components which use @defer in their templates and provides us with an API which allows us to control and change the state of any given @defer block!

All @defer blocks are in "paused" (placeholder) state by , and we can manually set them to one of the intuitive values of the DeferBlockState enum:

  • Placeholder
  • Loading
  • Complete — displays the deferred component itself
  • Error

Then given a ParentComponent...

@Component({
  // ...
  template: `
    @defer {
      <my-org-heavy />
    } @placeholder {
      <p>Placeholder...</p>
    } @loading {
      <p>Loading...</p>
    }`
})
export class ParentComponent {}

We can write a test...

describe('ParentComponent', () => {
  let fixture: ComponentFixture<ParentComponent>;

  beforeEach(async () => {
    // Standard TestBed setup...
    fixture = TestBed.createComponent(ParentComponent);
  });

  it('should create', async () => {
    expect(component).toBeTruthy();

    const deferBlockFixture = (await fixture.getDeferBlocks())[0];

    expect(fixture.nativeElement.innerHTML).toContain('Placeholder...');

    await deferBlockFixture.render(DeferBlockState.Loading);
    expect(fixture.nativeElement.innerHTML).toContain('Loading...');

    await deferBlockFixture.render(DeferBlockState.Complete);
    expect(fixture.nativeElement.innerHTML).toContain('<my-org-heavy />');
  });
});

Wrap up

Angular 17 and lazy loading of standalone components with @defer is amazing! ❤️

I hope you enjoyed the article and learned about the new Angular @defer block! The article can serve as a quick reference for the API which is listed neatly together with common examples anf use cases.

I hope that the interactions and best practices sections will save you some headache when figuring out all the non-obvious interactions between the @placeholder and @loading blocks and prefetch statements!

Do you have different experience with @defer or like to use other approaches and best practices? Please leave a comment bellow and I might add it to the article to share with other folks in the community!

Also, don't hesitate to ping me if you have any questions using the article responses or Twitter(X) DMs @tomastrajan or get in touch!

And never forget, future is bright

Obviously the bright Future

Obviously the bright Future! (📸 by Tomas Trajan in Dolomites 🇮🇹)

Do you enjoy the theme of the code preview? Explore our brand new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.

Do you enjoy the content and would like to learn more about how to ensure long term maintainability of your Angular application?

Angular Enterprise Architecture eBook

Angular Enterprise Architecture eBook

Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.

This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!

We will never share your email with anyone else and you can unsubscribe at any time!

Emails may include additional promotional content, for more details see our Privacy policy.

Responses & comments

Do not hesitate to ask questions and share your own experience and perspective with the topic

Tomas Trajan - GDE for Angular & Web Technologies

Tomas Trajan

GDE for Angular & Web Technologies

I help developer teams deliver successful Angular applications through training and consulting with focus on NgRx, RxJs and Nx!

Ein Google-Developer expert für Angular und Webtechnologien, der als Berater und Angular-Trainer arbeitet. Derzeit unterstützt er weltweit Teams in Unternehmen bei der Implementierung von Kernfunktionalitäten, Architekturen, der Einführung von Best Practices, und der Optimierung von Arbeitsabläufen.

Tomas ist ständig bestrebt, seinen Kunden und der breiteren Entwicklergemeinschaft einen maximalen Nutzen zu bieten. Seine Arbeit wird durch eine umfangreiche Erfolgsbilanz unterstrichen, in der er populäre Fachartikel veröffentlicht, Vorträge auf internationalen Konferenzen und Meetups hält und zu Open-Source-Projekten beiträgt.

52

Blog posts

4.7M

Blog views

3.5K

Github stars

612

Trained developers

39

Given talks

8

Capacity to eat another cake

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like Modern Angular !

Stärken Sie Ihr Team mit unserer umfassenden Erfahrung

Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und würden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.

or