Using defer in Angular 17 to implement lazy loading

mangelosanto

Matt Angelosanto

Posted on December 12, 2023

Using defer in Angular 17 to implement lazy loading

Written by Lewis Cianci✏️

Most of the time, there’s no sense in procrastinating. If you can do something now, then you should probably do so and stop putting it off. However, in software development, the reverse is typically true, which is why we use a strategy called lazy loading.

Angular 17 introduced a new defer block that lets you lazy load content based on specific conditions or events. In this tutorial, we will discuss why this is important and how to implement lazy loading strategically in our Angular applications.

We’ll build a simple app to get a hands-on look at what makes defer so great. The complete code for the demo is available on GitHub. Let’s get started!

Why use lazy loading in Angular apps?

We can load our entire application in one hit, but is it going to lead to an excellent UX? Probably not. Loading only the parts of your app that the user is actually going to use, then loading other parts of your application later, is a win for everybody.

So, we should try to split up larger applications and deliver only the required parts of the application to the user. Why, though? There are a few reasons:

  • Typically, components load when they are invoked through the template. So, if you have a moderately complex Angular application and you depend on a few components on the same page, then just letting all components load and initialize at the same time can lead to a janky app
  • A given webpage might be quite long, full to the brim with information for the user to scroll through. However, if the user doesn’t scroll, all of the loaded data is thrown away once the user navigates to a different page. It would be better to only load the bits that we know the user will view
  • Packing our entire app into one single monolithic file will increase the time it takes for the app to load for the first time, and we will re-incur this penalty every time we update the application. If our app is broken into smaller components at build time and then update one of those components, only the updated parts will get re-downloaded again

Normally, splitting an app up into smaller pieces which are then dynamically loaded at runtime is not an easy task. After all, we are producing the main entry point for our app, and then loading specific parts of the app at a later point in time.

With Angular 17, this all becomes far easier and more ergonomic. But to understand the value, we need to get into the nitty-gritty of how Angular builds and packages applications.

Honestly, it can get a little dry — I’d rather be building cool apps than digging through a minified JavaScript file! But if we robustly understand the value of @defer, then we can really understand what it can bring to our app.

Building Angular apps without defer

Normally, when you fire off the ng build command, your entire app gets built into a few different files. The result of this process looks like the below: Example Of Typical Angular App Getting Built Into Various Files Shown In File Finder Window In Alphabetical Order As you can see, we have here:

  • Assets such as images that our app uses in a folder
  • The favicon for the application
  • The index.html file for our browser to load

Then, we have the actual JavaScript and CSS that form our application:

  • main.js — The Angular framework and our code put together to form what is run on the client side
  • polyfills.js — If a browser lacks a certain feature, the given polyfills will help your app to still run
  • styles.css — The styles for your web app

While the polyfills and styles files are quite small, our main.js file is fairly big by comparison. This is for a simple app, so it’s not hard to imagine how large some Angular applications could become.

Faced with this, there are only really two options. The first is that we expect our users to deal with ever-increasing bundle sizes, as well as to wait longer and longer for the app to load the first time. Our second option is to split up our application into smaller chunks.

To demonstrate this, let’s make a simple app called “Rate My Ducks” in Angular with Angular Material. The idea is that the user will be presented with a simple list of ducks and can open each duck’s details to rate them. Optionally, the user can sign up for a newsletter.

Our simple app will look something like this: Overview Of Rate My Ducks Demo App Showing Blue Bar At Top With App Title And Rows Of Three Duck Card Components With Titles, Images, Fun Fact About Ducks, And Details Text Button Clicking on DETAILS will open up the chosen duck, give the user a suggested action to do with the duck, and allow them to rate the given duck: Duck Card Component Details Page Open To Show Duck Image, Activity Suggestion, And Option To Rate Duck Out Of Five Stars At the moment, this app doesn’t use deferred views. So, let’s go ahead and run ng build --stats-json. This will produce a stats.json file which we can send to the esbuild Bundle Size Analyzer for analysis. In our case, the result looks like this: Report From Esbuild Bundle Size Analyzer Of Stats Json File Showing Various Parts Of App By Relative Component Size What on earth are we looking at? Well, we’re looking at all the various parts of our simple application, and how big each component is. At the top left, we can see that our app has only imported the parts of Angular Material that are needed, like form-field, dialog, and so on.

In all of this, however, where is our actual application code? Well, it’s the very thin red rectangle towards the right of the picture. When we click on that rectangle, we can see our application in a format that makes more sense to us: App Code From Previous Report Expanded To Show App In More Familiar Format Every single person who visits the soon-to-be internet sensation of “Rate My Ducks” will get everything that I’ve made for the app.

The people who click into a duck will, of course, get the duck viewer and the ability to rate. However, the users who don’t click into a duck will also get all of the same files as well, even if they never actually view the information.

Within the context of this app, this might not be a significant problem. It’s not so hard to apply this logic to a much bigger application, though.

For example, if your app has an administrative control panel that only one percent of your user base would see, all of that code would get bundled and sent to everyone by default. That leads to bigger payloads for the end user, which can increase memory usage and wait times, with no benefit to them.

There’s no sense in bringing in parts of an application that the user is never going to make use of. So, there’s a benefit in breaking our app into smaller pieces, and only loading those pieces when the user requests them.

But how on earth would we dynamically load new pieces of our app just in time for users to view without disrupting the experience? And how would we do that in a way that was maintainable and scalable?

If we were doing this manually, it would not be easy. But with Angular 17 and defer, it becomes surprisingly doable.

Without defer, our main.component.html file looks like this:

<div class="main-container">
  <div class="newsletter">
      <app-newsletter-signup></app-newsletter-signup>
  </div>
  <div class="flex-container">
    @for (duck of this.ducks;track duck.file) {
        <app-duck-card [duck]="duck" class="flex-item"></app-duck-card>
    }
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Cleverly, the Angular compiler sees our use of the app-newsletter-signup and app-duck-card components and bundles them into our final payload. What happens if we surround the app-duck-card with @defer? Let’s try it out:

<div class="main-container">
  <div class="newsletter">
      <app-newsletter-signup></app-newsletter-signup>
  </div>
  <div class="flex-container">
    @for (duck of this.ducks;track duck.file) {
      @defer{
        <app-duck-card [duck]="duck" class="flex-item"></app-duck-card>
      }
    }
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Now, let’s re-run the build process. However, this time, let’s run the command like so:

ng build --stats-json --named-chunks
Enter fullscreen mode Exit fullscreen mode

The --named-chunks option will give our output more understandable names, so we can easily identify what component is going into which file. We receive this output: Named Chunks Option Enabled When Building Project To Show Output With More Understandable Names What does the esbuild analyzer make of this? Let’s see: Second Esbuild Analyzer Report Showing App With New Duck Card Component Javascript File We have our existing chunks, and everything looks familiar here. However, we now have a new duck-card-component JavaScript file. This file contains our standalone duck card component, which is loaded after our initial application startup, courtesy of the @defer functionality.

If we hosted this application now, we would see our initial bundle load followed by our separate duck component, which would then load in our duck images. In Chrome DevTools, it looks like this: Screenshot Of Chrome Devtools Labeled To Show Main App Loading Before Duck Component Loads Later After Which Images Begin To Load Just adding this @defer statement breaks up our app into two separate pieces. The application load process now looks a bit like this:

  1. The application starts
  2. The initial page is loaded
  3. Our deferred component is then lazy-loaded

At this stage, instead of loading our entire app immediately, we’re loading a part of our app first, and then almost immediately loading the rest of it. That’s not necessarily a bad thing, but if we’re here to optimize, then we want to be a little bit cleverer about this.

Choosing when to load the component

The next step in this journey is defining when we want our component to actually load. By default, they will load during the parent’s component template execution. But we can be very precise about when we want the deferred component to actually load.

We can load components:

  • on idle — When the browser has become idle, and has stopped processing other tasks. We can achieve this by using the requestIdleCallback API
  • on viewport — When the content is scrolled into view
  • on interaction — When the user clicks on the placeholder or another specified element
  • on hover — When the user hovers over the placeholder element, or another specific element
  • on immediate — Retrieve the deferred chunk immediately
  • on timer — Wait a predefined amount of time before fetching the component

Since we are now making a decision about something that will happen in the future, we need to tell Angular what to render at these various stages:

  • Before the component has begun fetching
  • While the component is being retrieved
  • If the component throws an error

Within our template, this basically boils down to the following:

  @defer {
        <large-component />
      } @placeholder {
        Placeholder
      } @loading {
        Loading...
      } @error {
        Something went wrong :(
      }
Enter fullscreen mode Exit fullscreen mode

If we wanted to load the component once it had scrolled into the user’s viewport, our @defer statement would change to @defer (on viewport).

Better still, we don’t have to commit to just one particular condition. For example, if we wanted to load a component after two seconds, or if we wanted to skip the wait once it enters the viewport, we could write our @defer block like so:

@defer (on viewport; on timer(2s))
Enter fullscreen mode Exit fullscreen mode

Even with the @placeholder element, we can specify a minimum amount of time for it to display before the loaded component is shown. This is to prevent a placeholder from flickering quickly at page load time and possibly changing the page layout.

Gradually, we begin to see how ergonomic this API is, and how simple it could be to carry out otherwise complex deferred loading operations.

There’s one last thing to consider. As well as being able to load a component in response to a certain browser event or timer, we can also instruct Angular to load the component when a given variable becomes truthy.

For example, if we wanted to load large-component after two seconds, or after the user has completed a form, we could write the following:

@defer (on timer(2s); when formComplete)
Enter fullscreen mode Exit fullscreen mode

Whichever happens first — the timer running out or the condition becoming true — will make the component render. However, the component will not disappear if the condition becomes false again — it’s a one-way switch.

If we explored every single permutation of using the on and when statements, this article would probably be overly long. Instead, let’s take more of a practical approach in showing how this works so you can apply it in your own apps. We’ll use these statements to streamline the load of our Rate My Ducks app.

Deferring a component load on viewport entry

The template for our main duck page shows a list of ducks and also has a component where a user can sign up for a newsletter. Let’s go ahead and make a change to defer the load of the newsletter component to when the user scrolls far enough for the component to appear in the viewport:

<div class="flex-container">
  @for (duck of this.ducks;track duck.file) {
      <app-duck-card [duck]="duck" class="flex-item"></app-duck-card>
  }
</div>
<div style="padding: 50px; max-width: 1024px">
  @defer (on viewport){
    <app-newsletter-signup></app-newsletter-signup>
  } @placeholder (minimum 2000ms) {
    <div>Loading..</div>
  }
</div>
Enter fullscreen mode Exit fullscreen mode

If we build our app and then host it locally, we get the following result. Note Chrome DevTools open to show network requests on the right-hand side: Demo Of Deferring Component Load On Viewport Entry With App Open Next To Chrome Devtools. User Showing Scrolling Through App Down To Newsletter Component, Which Loads As The Component Enters The User's Viewport When we begin scrolling through our app, most of the app loads normally. However, the required Angular module for the newsletter component is only loaded and displayed when we get to the bottom, where our newsletter component is.

If we were to check this loaded component via the esbuild analyzer, we can see that this individual JavaScript file only contains the code for our text field and our component code: Checking Loaded Newsletter Component Via Esbuild Analyzer. Report From Analyzer Shows This Individual Javascript File Only Contains Code For Text Field And Component Code The newsletter component will never load for users who never scroll all the way down to it, so we’re not wasting resources or bandwidth unnecessarily.

Deferring component loads on hover

Another option we have is to defer component loads until an element is hovered over or clicked on. So, within our list of ducks, let's defer our duck card component load and wait until the user hovers over the element before loading the specific component.

We don’t want the page to reflow dramatically when the image loads, so our placeholder should roughly be the same size as our component will be when it loads:

@for (duck of this.ducks;track duck.file) {
  @defer (on hover) {
    <app-duck-card [duck]="duck" class="flex-item"></app-duck-card>
  } @placeholder {
    <div style="height: 300px; width: 300px;">Wait for the duck...</div>
  }
}
Enter fullscreen mode Exit fullscreen mode

If we build and serve our app, our application now looks like this: Demo Of Deferring Component Load On Hover. Initial Page Is Loaded With Placeholders For Every Duck Card Saying Wait For The Duck. User Shown Hovering Over Each Component To Load It With Chrome Devtools Showing How Component Is Fetched And Loaded Only After Hover The first time our duck component is hovered over, the appropriate component is retrieved from the server. On subsequent hovers, the same component — which has already been downloaded — is re-used. Finally, images are only retrieved once the component is loaded, which can further reduce the download of unused resources.

Considerations while using defer in Angular

Deferred views are a powerful and ergonomic extension of existing Angular functionality, so it becomes tempting to surround every component with @defer. But there are a few things to think about before we do that.

Nested deferred views

First, if we re-use components throughout our application — as we should — we might wind up with nested deferred views.

That’s not a bad thing outright, as parts of our app are streamed in as needed. However, if we use timers or other long-running operations to describe when our views should appear, these can quickly add up.

For example, imagine we had a component nested three components deep, and every component was inside a defer block with a timer of two seconds. Our final component would render after a total of six seconds.

As applications become more complex, it could be easy to end up waiting too long for a page to load completely.

Layout changes

Secondly, our loaded component could be a completely different size from our placeholder. This could cause the page to re-flow or dramatic layout changes, which Angular considers a bad practice.

Where this occurs, it would make sense to make our placeholder the same size as our loaded component. We did this earlier when deferring the duck card component from loading until the user hovers over it.

Prefetching components

Finally, Angular also gives you the option to prefetch your components based on similar conditions, such as time or viewport visibility.

Prefetching a component means downloading the component, but delaying execution until it’s actually called in a @defer statement. For example, you can prefetch a large component on browser idle before it’s executed at a later stage.

Combining prefetching with defer blocks can help improve performance, particularly for larger components. Once the component is finally rendered, having it prefetched can reduce the load time from the user’s perspective.

Wrapping up

Deferrable views within Angular make the overall framework a much more compelling option for modern web apps. The defer feature can help us optimize the delivery of our apps to end users.

Whether you are new to Angular or are a seasoned developer, learning how to use deferred views is very useful. Bringing in larger dependencies isn’t as big of a deal when you know they’ll only be loaded when a particular component uses them.

At any rate, if you already have an Angular app, it’s definitely never been a better time to cast an eye over it and see if you can improve the load times of your app by using deferred views. If you just want an easy sandbox to download and play around in, feel free to clone the sample app so you can see how it all goes together.


Experience your Angular apps exactly how a user does

Debugging Angular applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Angular state and actions for all of your users in production, try LogRocket.

LogRocket Signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your site including network requests, JavaScript errors, and much more. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred.

The LogRocket NgRx plugin logs Angular state and actions to the LogRocket console, giving you context around what led to an error, and what state the application was in when an issue occurred.

Modernize how you debug your Angular apps — start monitoring for free.

💖 💪 🙅 🚩
mangelosanto
Matt Angelosanto

Posted on December 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related