Besides some minor updates concerning Server Side Rendering (including things as making the great new Hydration feature stable, renaming the universal package to @angular/ssr
and adding SSR support to the CLI's ng new
command) the biggest upgrade for us performance enthusiasts in Angular 17 is definitely the new block template syntax including the defer block feature. By the way, the new template syntax is also beneficial for the runtime performance - we'll cover that topic in another blog post soon 😏
Until Angular 16 it was a bit complicated to dynamically lazy load (now called defer) components, without having to use the Angular router's loadChildren
(for a module or an array of components) or loadComponent
(for just one standalone component). Since Angular 13 we didn't need a ComponentFactoryResolver
to create the component anymore, but we still had to use a ViewContainerRef
and something like an async / await
block to dynamically load, create and insert the component into the DOM.
With @defer
we now have an elegant and intuitive option to delay the loading of components until they are needed. This is especially useful for components that are not visible on the initial screen (so-called above-the-fold) of our web app. The @defer
feature has the largest impact if we can use it for heavy components that include large third party packages like feature-rich tables, charts or some export functionality (e.g. PDF generator), because those packages can then also be deferred and thus excluded from the eagerly loaded main bundle.
Why is Initial Load Performance so important?
We recently blogged about Why Initial Load Performance is so important, focussing on SSR.
Deferrable Views, also known as @defer
blocks, are a lightweight innovation to reduce the initial bundle size and thus further improve the Initial Load Performance of our Angular app.
We also wrote about how to measure the Initial Load Performance.
Deferring some of your components should specifically improve First Contentful Paint (FCP), Largest Contentful Paint (LCP) and even Time to First Byte (TTFB).
However, be careful not to increase the Cumulative Layout Shift (CLS) by deferring components that are visible on the initial above-the-fold screen.
You can achieve this by using placeholders (see below for @placeholder
and @loading
) with fixed width and height properties - just like you'd do for lazy loading images with the help of NgOptimizeImage
.
How to dynamically load components with Angular 13 - 16
In my Performance Workshop 🚀 I always show(ed) how to dynamically load components with Angular 13 - 16.
Firstly, we need to get a ViewContainerRef
in the components view template. Since we don't want to create an HTML element, we use an ng-container
:
<ng-container #viewContainer />
Important note: Since Angular 15.1.0 we can use self-closing tags in Angular - also for components without content.
Secondly, we need to fetch the ViewContainerRef
via a @ViewChild
using the template reference #viewContainer
in the component class:
@ViewChild('viewContainer', { read: ViewContainerRef }) viewContainerRef!: ViewContainerRef;
Thirdly, we can use async / await
to dynamically import a component and insert it into the DOM:
async ngAfterViewInit() {
const { LazyComponent } = await import('./lazy/lazy.component');
const lazyComponentRef = this.viewContainerRef.createComponent(LazyComponent);
}
As a result the Angular Compiler will create a new chunk for the LazyComponent
and the app (well, the browser) will load the bundle on demand.
While this is still a working approach, it is a bit heavyweight and less powerful than using the new @defer
syntax.
Important note: To fully support deferring our LazyComponent
must be a standalone component.
How to dynamically load components with Angular 17's @defer
With Angular 17 we can use the fancy @defer
syntax. The syntax is very similar to the new @if
and much, much easier to use:
@defer {
<aa-lazy-component />
}
That's all there is to it 🥳 - the Angular Compiler will again create a new chunk for the LazyComponent
and the app (again, the browser) will load the bundle on demand.
But that's not the end of the story (nor this post 😉). We can additionally leverage the control of the lazy loading process by using built-in and even our own triggers.
Triggers
The @defer
block in the provided context is used to replace placeholder content with lazily loaded content.
Two options for triggering this swap are specified: on
and when
.
on
on
specifies built-in trigger conditions using events like interaction or viewport.
@defer (on viewport) {
<aa-lazy-component />
}
A list of on
events provided in Angular 17:
-
on idle
triggers once the browser has reached an idle state (detected using the requestIdleCallback API).
This is the default behavior with a defer block, so no need to write it explicitly.@defer { <aa-lazy-component /> }
-
on viewport
triggers when the specified content enters the viewport (using the IntersectionObserver API).
This could be the placeholder content.@defer (on viewport) { <aa-lazy-component /> } @placeholder { <img width="420" height="420" alt="lazy placeholder" src="placeholder.avif" /> }
- Alternatively, you can specify a template reference
#variable
.
<div #viewportVariable>Hello!</div> @defer (on viewport(viewportVariable)) { <aa-lazy-component /> }
- Alternatively, you can specify a template reference
-
on interaction
triggers when the user interacts with the specified element throughclick
orkeydown
events.@defer (on interaction) { <aa-lazy-component /> }
- Alternatively, you can also specify a template reference
#variable
.
<button #interactionVariable>Hello!</button> @defer (on interaction(interactionVariable)) { <aa-lazy-component /> }
- Alternatively, you can also specify a template reference
-
on hover
triggers on hover (actuallymouseenter
orfocusin
).@defer (on hover) { <aa-lazy-component /> }
- Alternatively, you can again specify a template reference
#variable
.
<div #hoverVariable>Hello!</div> @defer (on viewport(hoverVariable)) { <aa-lazy-component /> }
- Alternatively, you can again specify a template reference
-
on immediate
triggers the deferred load immediately. Once the client has finished rendering, the defer chunk will start fetching right away. This is similar to asetTimeout
with a timeout of0
.@defer (on immediate) { <aa-lazy-component /> }
-
on timer
triggers after a timeout inms
ors
, working likesetTimeout
once the client has finished rendering.@defer (on timer(4200ms)) { <aa-lazy-component /> }
when
when
specifies an imperative condition as an expression that returns a boolean. If the condition returns to false, the swap is not reverted; it is a one-time operation.
class WhenDemoComponent {
condition = false;
trigger() {
this.condition = true;
}
}
@defer (when condition) {
<aa-lazy-component />
}
Multiple when
and on
triggers can be used together in a statement. They are always OR conditions so the swap occurs if either condition is met.
Additional features of the defer block
Beyond easily specifying triggers, the @defer
block offers the following useful features:
prefetch
@defer
allows to specify conditions when prefetching of the dependencies should be triggered.
It works similarly to the main defer conditions, and accepts when
and/or on
to declare the trigger.
In this example, the prefetching starts when a browser becomes idle.
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
}
@placeholder
A placeholder content to be displayed until the @defer
block is loading.
By default, defer blocks remain inactive until triggered. The optional @placeholder
block displays content before activation, which is then replaced by the main content after loading. Note that placeholder block dependencies are eagerly loaded, and various content types are supported.
Important note: When rendering an application on the server (either using SSR or prerendering, now called SSG), @defer
blocks will ignore triggers and always render the @placeholder
(or nothing if no placeholder is specified).
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
} @placeholder (minimum 500ms) {
<img width="420" height="420" alt="lazy placeholder" src="placeholder.avif" />
}
The @placeholder
block accepts an optional minimum
parameter to specify the amount of time that it should be shown.
@loading
A loading content to be shown to users while the @defer
block is loading.
The optional @loading
allows you to declare content that will be shown during the loading of any deferred dependencies. For example, you could show a loading spinner. Similar to @placeholder
, the dependencies of the @loading
block are eagerly loaded.
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
} @placeholder (minimum 500ms) {
<img width="420" height="420" alt="lazy component placeholder" src="placeholder.avif" />
} @loading (after 500ms; minimum 1s) {
<img width="420" height="420" alt="lazy is loading spinner" src="spinner.avif" />
}
Just like @placeholder
, after
and minimum
exist to prevent fast flickering.
@error
An error content to be displayed in case the @defer
block encounters an error during loading.
The optional @error
allows you to declare content that will be shown if deferred loading fails.
@defer (on viewport; prefetch on idle) {
<aa-lazy-component />
} @placeholder (minimum 500ms) {
<img width="420" height="420" alt="lazy component placeholder" src="placeholder.avif" />
} @loading (after 500ms; minimum 1s) {
<img width="420" height="420" alt="lazy is loading spinner" src="spinner.avif" />
} @error {
<p>Why do I exist?</p>
}
Automated control-flow migration
I also want to mention that one of the Angular teams' goals of the built-in control flow was to enable completely automated migration. Give it a try in your app:
ng update
ng g @angular/core:control-flow
And now make sure to try out the new @defer
block feature 👏
Conclusion
Angular 17's introduction of Deferrable Views, particularly the new @defer
block syntax, marks a significant leap in simplifying the dynamic loading of standalone components. @defer
not only streamlines the process but also enhances Initial Load Performance by deferring heavy components, such as those with large third-party packages, until they are needed.
Leveraging the built-in on
and custom when
triggers along with the prefetch
feature and the @placeholder
, @loading
and @error
states, further empowers developers to optimize the user experience with a more responsive and efficient web application.
References
- What's new in Angular 17 by Manfred Steyer
- Introducing Angular 17 by Minko Gechev
- Deferrable Views with Jessica Janiuk on Angular YouTube
- Deferrable Views in the Angular Docs
Performance Deep Dive Workshop
If you want to deep dive into Angular performance, we offer a dedicated Performance Workshop 🚀 - both in English and German.
This blog post was written by Alex Thalhammer. Follow me on GitHub, X or LinkedIn.