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!
Tomas Trajan
@tomastrajan
14.11.2023
13 min read
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)!
💡 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)
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
andNgModule
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 returnstrue
orfalse
(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 executionidle
— (default) Angular will lazy load the component on the first availablerequestIdleCallback
(browser API) which allows us to perform background and low priority work on the main event looptimer(delay)
— after a specified delayviewport (target)
— when the@placeholder
(described below) or optionaltarget
are in the viewport (detected using browserIntersectionObserver
API)hover (target?)
— when the@placeholder
or optionaltarget
are hovered by the user, for this purpose Angular considers themouseenter
andfocusin
DOM eventsinteraction (target?)
— when the@placeholder
or optionaltarget
are interacted with by the user, for this purpose Angular considers theclick
andkeydown
DOM events
⚠️ Duplicate on handlers of the same type are not allowed! For example,
on hover
(placeholder) together withon hover(someTarget)
can't be used together on a single@defer
block! Currently, there can be only a singletarget
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 the100ms
, 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
withminimum
+ fast loading time = we won't see the@loading
block@loading
withafter
+ fast loading times = we won't see the@loading
blockon 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
orfalse
(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
— useidle
insteadidle
— good candidate for many use casestimer(delay)
— use instead ofidle
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, theidle
might get in a way of that, so we might want to postpone prefetching to a "safer" time after thatviewport (target)
— makes sense to use withtarget
which appears before the@defer
on the same page as then it's better to just do a standardon viewport
trigger instead of theprefetch
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 theprefetch
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 theprefetch
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 -> …
- 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!
- 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 AngularRouter
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:
- 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
- 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 likeon viewport
oron hover
as user might reach component faster than the specified delay and using only theon 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 prefetching200px
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 itselfError
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! (📸 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
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
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!
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
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 !
Top 10 Angular Architecture Mistakes You Really Want To Avoid
In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!
Tomas Trajan
@tomastrajan
10.09.2024
15 min read
Angular Control Flow
A new modern way of writing ifs, if-else, switch-cases, and even loops in Angular templates!
Kevin Kreuzer
@kreuzercode
24.10.2023
5 min read
How to migrate Angular CoreModule to standalone APIs
Let's learn how to migrate commonly used Angular CoreModule (or any other Angular module) to standalone APIs to fully embrace standalone project setup!
Tomas Trajan
@tomastrajan
05.09.2023
9 min read
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.