Using @Defer DeferViews in Angular 17

Using @defer to optimize and reduce bundle easy

ยท

7 min read

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 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 as false.

  • 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.

ย