If you ever run into a situation where your Angular application is taking too long to load, then this is the right post for you.

We are going to show you how to tackle this problem in a systematic, evidence-based approach.

We will first learn how to generate a performance profile for your bundle, to help identify the major performance culprits in your application.

From there, we are going to learn a couple of different ways to split up your application into smaller chunks, using the very latest performance features of Angular:

  • standalone lazy-loading
  • partial template loading

So without further let's get started with this Angular Performance Tuning deep dive!

Table of Contents

In this post, we will cover the following topics:

  • What type of metrics should you collect?
  • Setting up the source map explorer package
  • Make your application fully lazy-loaded in a simplified way using standalone components
  • Partial template loading with @defer
  • The bottom line

If you prefer to learn this content in video format, here is the video version of this post:

Angular Performance Tuning Crash Course

What type of metrics should you collect?

Let's start with the most important first!

The worst thing that we can do when trying to optimize your application is to fly blind, and base ourselves on gut feelings and trial and error.

The first thing that we need to do is to collect some data, some metrics.

And for that, we are going to use the source map explorer package.

So your first step should always be to generate some sort of bundle profile report.

This is a disk space usage visual report, that tells you what libraries and Angular components are taking the most space in your Javascript bundle.

This will allow you to know what parts of your application are taking the most space. This is essential.

In order to collect this type of metrics, you will need the following profiling tool.

Setting up the source map explorer package

The first thing that we should do is to install the following package in our codebase:

npm install -g source-map-explorer

So once you have the package, you now need to use it to generate a report.

Here is the npm script that I usually use to generate my bundle report:

{
    ...
  "scripts": {
    ...
    "bundle-report": 
       "ng build --configuration production  
       --source-map && 
       source-map-explorer dist/browser/*.js"
  },
}

This npm script report works in two parts:

  • First, we need to do a production build of our application. So we are going to run ng build --configuration production

  • once the uncompressed bundles are generated in your local file system, we are going to run the source-map-explorer package to generate a report.

I recommend that you add a similar npm script to your project so that you can easily generate a bundle space usage report at any time that you want.

So once you open the report, you are going to see something like this:

Angular performance tuning of an application bundle

Don't get scared by these bundle sizes, the actual main production bundle is much smaller than this. Remember that these bundles are uncompressed.

Once you have this report, you can start identifying components and libraries that you can lazy load into a separate bundle.

Look for heavy dependencies that are rarely used, like PDF-generation libraries, chart libraries, etc.

These tend to be used rarely by the user, so they are ideal candidates for code splitting.

Make your application fully lazy-loaded

Now you know what parts of your application take up the most space, so the next step is to make your application as lazy loaded as possible.

Ideally, every single screen of your application should be loaded in a separate bundle.

This is what I like to call a fully lazy-loaded application. But how do you make your application fully lazy-loaded?

The best way that you have available right now is to leverage the simplified lazy-loading features of standalone components.

So let me show you how simple it is to take a route of your router and make it lazy-loaded using Standalone components.

Here is a route with a screen that is currently not lazy-loaded:

...
export const routes: Routes = [
  {
    path: 'courses/:courseUrl',
    component: WatchCourseComponent
  }
]
...

Notice that if you were using NgModule components, making this screen lazy-loaded would be quite a chore.

But luckily, this application is already migrated to standalone components!

So all we have to do to make this screen lazy-loaded is to switch the use of component to loadComponent, like so:

  {
    path: 'courses/:courseUrl',
    loadComponent: () => 
      import('./watch-course/watch-course.component')
        .then(mod => mod.WatchCourseComponent),
  }

And it's that simple!

Now this route is lazy-loaded, and the WatchCourseComponent and all its dependencies are off the main bundle.

So if this screen needed a heavy dependency like a PDF or chart library, now all those heavyweight dependencies are now off your main application bundle, and will be loaded only if necessary.

Remember, this only worked because the WatchCourseComponent is a standalone component.

If you are looking to learn more about standalone components and how to migrate to them, check out this in-depth guide that I wrote:

Angular Standalone Components: The Complete Guide

You can now go back to your routing configuration, apply loadComponent everywhere, and you will have a fully lazy-loaded application where every single container screen has its own separate bundle.

Your main bundle should now contain the Angular framework and very little else, and hardly any heavy third-party dependencies.

This should already be enough to improve a lot the performance of your application, but we can go even further.

Partial template loading with @defer

Now let's go ahead and let's introduce another performance optimization technique, which is similar to lazy loading.

So going back here to our codebase, we are already lazy loading this component, right?

  {
    path: 'courses/:courseUrl',
    loadComponent: () => 
      import('./watch-course/watch-course.component')
       .then(mod => mod.WatchCourseComponent)
  }

But now, imagine that this component WatchCourseComponent is still fairly large.

This component is a course player that supports different types of lessons:

  • audio lessons
  • digital downloads
  • assessments, etc.

And that is all part of the watch course screen.

But now imagine that a certain course only has video lessons.

So it doesn't have audio lessons, etc.

Then in that case loading the code for the audio lesson would be unnecessary and premature.

Ideally, we should only load the code for the audio lesson whenever the user actually clicks on an audio lesson, and not before.

This way, we avoid loading code that we might never use for that user session!

But the component as it stands right now will either load fully or not at all.

So what we need is some sort of partial template loading, that would allow us to load parts of a screen only when and if needed.

And we can do just that, using the powerful @defer feature of Angular:

@defer(when lesson.type == 'audio') { 

  @if(lesson.type == 'audio') {

    <audio-player [lesson]="lesson" />

  } 
}

The way that this works is that defer is going to make anything inside the defer block to get loaded on a separate bundle, much like the router-based lazy loading mechanism.

The defer block is going to make the code inside it lazy-loaded, and the if block is going to conditionally show or hide the audio player. The defer condition @defer(when lesson.type == 'audio') is going to make the audio player lazy-loaded only if the lesson is of type audio.

The result is that the code for the audio player is no longer part of the same bundle as its parent component the WatchCourseComponent.

Now with @defer, the code for the audio player will only get loaded if we navigate to an audio lesson.

The new Angular @defer functionality is very powerful, to learn more about it in detail, check out this in-depth guide that I wrote:

Angular @defer: The Complete Guide

The bottom line

So in summary, this is how you can do the performance tuning of your Angular application:

  • Step 1: always make sure that you create some sort of a report that allows you to see what is taking up space in your application. The source-map-explorer package is a great tool for this.
  • Step 2: I recommend that you upgrade to Standalone components, and leverage the simplified lazy-loading feature loadComponent. With it, it's super easy to make every single screen of your application lazy-loaded.
  • Step 3: If you still have components that are too large, use @defer to do partial template loading inside your heavier components.

If you iteratively follow these steps and keep generating bundle reports to confirm your progress and identify new bottlenecks, you will be able to make your application as lean as possible.

And remember, if you prefer to see all this in action in video format, here is the video version of this post showing how to optimize a real production application:

Angular Performance Tuning Crash Course

Let me know in the comments if you have any questions or comments, I would love to help!