Using @Defer DeferViews in Angular 17
The Performance, this is one most important topics when we build applications, now days we work with huge amount of components and needs to think how to improve the bundle size in our apps.
The Angular team knows that, and launches great and amazing features for lazy loading called "defer views".
What is Defer Views?
The defer views is the easiest way to implement lazy loading and split our code into chunks to improve user performance, loading the code only when we really need it.
The defer view with @defer
block allowing us to load our components with less code and without imperative programming, making easy to lazy loading and easy to trigger our components based to default ways provide by Angular or custom to match with our business cases.
But, wait a minute? We already have lazy loading in Angular 😠!
We have a nice way to use lazy loading and create chunk-specific routing. For example, when the user navigates to a specific route, we then load a particular component.
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/home/home.component').then((h) => h.HomeComponent),
},
{
path: 'products',
loadComponent: () =>
import('./pages/products/products.component').then(
(p) => p.ProductsComponent
),
},
];
But when we want to load a component dynamic based to other events, we must to write a imperative code and combine, with few things and not easy to handle one or many states.
Today, we’re going to learn why we must to use @defer
in Angular 17 to reduce the bundlesize, load dynamic component easy!
Scenario
Let’s say we create a landing page where we show a letter with best Angular 17 features and show the Kendo Ninja, the app have the following components:
Letter: Show a list of links of Angular new features.
Ninja: It's show a the Kendo UI Ninja.
When the user checks, the NinjaComponent
appears. By using the @if
, we change the boolean accepted
to true, and it shows the <ninja/>
component.
The code looks something like this:
<letter/>
<p>Do you accept?</p>
<input (change)="accept()" type="checkbox">
@if(accepted) {
<ninja/>
}
Let's clone the repo and install the dependecies, to see the current code:
git clone https://github.com/danywalls/learn-defer-views-angular.git
cd learn-defer-views-angular
npm i
After that we can see special details in the output, a single main.js
with polyfills.js
and styles.css
.
After that, run ng serve -o
to see the application at http://localhost:4200, and open the developer tools (F11) and go to the tab. We see a bunch of files, but one is important. The main.js
contains the bundle of our app with all components in a single file.
Maybe you ask your self, why we are sending components that maybe the user don't want to see or don't need to see only in special cases ?
In this moment is where we need to start to implement a defer loading "manual" to load our component, loading a specific .js
(or chunk). Let's do it!
Manual Lazy Loading 😞
Open the app.component.ts
, declare a new public variable ninja of type, then change the signature of the accept() method to a promise.
In the accept method, using dynamic import, set the variable jump with the dynamic import of NinjaComponent
.
async accept(): Promise<void> {
const { NinjaComponent } = await import('./components/ninja.component');
this.ninja = NinjaComponent;
}
The final code in app.component.ts
looks like:
import {Component, Type} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {NgComponentOutlet} from "@angular/common";
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NgComponentOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
public ninja!: Type<any>;
async accept(): Promise<void> {
const { NinjaComponent } = await import('./components/ninja.component');
this.ninja = NinjaComponent;
}
}
Next in the app.component.html
, add the ng-container with the ngComponentOutlet
directive and set the value of jump.
Remember to import the NgComponentOutlet
, save the changes, and view the details in the output.
We got a new chunk, chunk-XQI5NWYI.js
for the NinjaComponent
and it is perfect!
Save changes and reload the page.
When we check the input, the chunk is downloaded and the component loaded! It's real optimization to only download the chunk when the user clicks.
But do you think this can scale in the future? What happens if tomorrow we want to add new cases to load the component like:
Load when the users scroll.
When the browser isn't busy.
Show a loading indicator while loading.
At this moment, it's not easy. This is when we must switch to defer
.
The Defer
Since Angular 17 we have the code block @defer
, allow write declarative code to make our lazy loading easy, showing the component when some condition or trigger match and the dependencies are ready.
The defer works combine with @error
, @placeholder
, @loading
and custom triggers.
Before we continue, let's refactor our code.
Add the
NinjaComponent
to the imports section.Declare a new variable
accepted
initialized asfalse
.Update the accept method to
void
, and inside, set the accepted value.
The final code looks like:
import {Component} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import {NgComponentOutlet} from "@angular/common";
import {NinjaComponent} from "./components/ninja.component";
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NgComponentOutlet, NinjaComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
accepted = false;
public accept(): void {
this.accepted = !this.accepted;
}
}
In the HTML markup, update to use the @defer
block; it replaces the area with a component when the browser state is idle by default, but we also combine it with triggers when
and on
.
For example, the defer
block will render when
the accepted
variable is is true.
@defer (when accepted) {
<ninja/>
}
Save changes, the output looks the same without all the boilerplate with the dynamic import
and the imports
and NgComponentOutlet
Everything continue working with lazy loading and the ninja component is loaded eagerly only when the user clicks as always.
but of course we also have the on
trigger to trigger the block based to a list of trigger:
on viewport
The deferred block is triggered when the element enters the viewport area, using the IntersectionObserver
API.
@defer (on viewport) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}
on interaction
The deferred block is triggered when the mouse has hover over the trigger area.
@defer (on interaction) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}
on interaction
The deferred block is triggered when the mouse has hover over the trigger area.
@defer (on hover) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}
timer
The deferred block is triggered after a specified time.
@defer (on timer(4s)) {
<ninja/>
} @placeholder {
<div class="ninja-placeholder"> </div>
}
We can continue show a nice examples of trigger but I recommend, read more about triggers and move foward to use @placeholder 😊 and @loading blocks.
The @placeholder and @loading
We are going to lazy load the Ninja
component when the user clicks to show it, but with a special process, we want to show a placeholder in the area where the Ninja appears. It help to us, avoid page shift saving this space for the Ninja component.
let's show the text "Are you ready" in the area where the NinjaComponent
will appear.
@defer (when accepted) {
<ninja/>
} @placeholder {
<p>Are you ready ?</p>
}
Save and reload the page, and you will see the message "Are you ready?" in the area where the NinjaComponent will appear.
Great, it works! Now let's enhance the loading process. For instance, I want to display the placeholder, but also show a message while the component is loading. To do this, I'll combine the loading block with the minimum parameter, setting it to 5 seconds.
@loading (minimum 5s) {
<h2>The Ninja is coming...</h2>
}
Save changes and play again! Everything works as expected. Remember, the placeholder and loading are optional blocks, but they are very helpful in improving the user experience.
Important: The code inside the @placeholder and @loading are bundle in the main.ts file.
Conclusion
We explored the advantages of using Angular 17's @defer feature to dynamically lazy-load components, which boosts performance by loading code only when it's needed. The @defer simplifies the process by removing the need for manual, imperative coding.
We also experimented with @defer using various triggers such as viewport entry, user interactions, and timers. Additionally, we enhanced user experience by using @placeholder and @loading blocks to effectively manage UI elements during load times.
Subscribe to my newsletter
Read articles from Dany Paredes directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Dany Paredes
Dany Paredes
I'm passionate about front-end development, specializing in building UI libraries and working with technologies like Angular, NgRx, Accessibility, and micro-frontends. In my free time, I enjoy writing content for the Google Dev Library, This Is Angular Community, Kendo UI, and sharing insights here.