Angular library lazy loading

Alain Boudard
6 min readMar 7, 2024

--

In this article, I wanted to focus on some of the options available with Angular to lazy load elements from a library, and how it affects the build and the bundles.

Our application will consist of a typical Angular workspace, nothing fancy, with at least one application and one library. So let’s build this. We will start with angular 16 and I will show why later.

npx -p @angular/cli@16 ng new ngLazy --create-application false

Then we create a standard application, it’s not standalone for now.

ng g application ng-app --style scss --routing true

The different ways to lazy load a library.

A feature in the application

First, let’s create a feature in our application. For now the feature will be module based, but that’s not so important regarding our issue here.

ng g m users --routing -- route users -m app-routing

We should have something like that :

const routes: Routes = [
{
path: '', component: HomeComponent,
},
{
path: 'users',
loadChildren: () => import('./users/users.module')
.then((m) => m.UsersModule),
},
j];

But the most important here is :

  • When accessing our feature through the route /users we should load the js bundle that contains the code of this module.
Angular routing lazy loading
  • When we change something inside one of the elements of this module, it should only rebuild the specific code (along with the runtime but only if you run ng servecommand, try and run ng build instead)
Angular build lazy loading

A feature in a library

Let’s create a library that will hold a whole feature ‘orders’ :

ng g library orders

Now we have to declare routing in this orders library, link the default route to the OrdersComponent, and then remove this component from the public-api surface exposition (you would want to expose only the component that will be explicitely used in the application).

Angular routing to a library feature

Then our workspace package.json will be configured to ‘build watch’ the library and in the same time, ‘ng start’ the application :

"scripts": {
"watch:orders": "ng build orders --watch --configuration development",
"start": "ng serve ng-app --port 4400"
}

Now, every time we change something inside one of the elements of this library, the build will be specific and produce the bundle that will be lazy loaded.

Angular build library lazy loading

Note : if you remove elements from your public-api.ts file, you should see the weight of the bundle evolve accordingly. So get rid of anything purely internal.

A more complex library

Now, imagine we don’t want only one feature in the library but another one, say bills feature. Let’s refactor the folders and create this new feature :

2 Angular features in a single library

Alright, this looks good, lets wire this up and display the appropriate link in our application.

Angular routing app to library features

The first thing to notice is the lack of specificity of the import statement, both ‘bills’ and ‘orders’ come from the same namespace, that’s not very cute. The other caveat shows when building the application :

Angular build library lazy loading

We have 2 features but only one bundle, when we update the code of either of the features, it’s all reprocessed. And indeed when we navigate to one of the routes, the first one does load the chunk, but it also loads the other one, even if we never access the route. How can we break this ?

Note: of course you can create as many libraries as you want to organise your features or “domains”, but let’s suppose that we want to keep our library unique, right ;)

Secondary entrypoints to the rescue

Secondary entrypoints in Angular libraries are a common feature that serve multiple purposes :

  • Have specific names for the imports in the applications
  • Separate the build and therefore make it faster
  • Create separated bundles that allow a better tree shaking in the applications
  • And as a result of all this, allow a single library to expose multiple features ready for lazy loading

To create a secondary entrypoint, it’s very easy :

  • Create a folder bills in the root of the library, with a subfolder src for conveniency
  • Create the 3 following files in this folder : public-api.ts index.ts ng-package.json
  • Move all the code from bills feature in the bills/src newly created.
Angular library secondary entrypoint

The content of the 3 root files are as follow :

Angular library secondary entrypoint configuration

Note that you still have to be careful with the components you decide to expose in your surface ‘bills’ public-api.

Now, once you refactored the library, for local build, you should ensure that whatever entrypoint in the library is handled by tsconfig file :

"paths": {
"orders": [
"dist/orders"
],
"orders/*": [
"dist/orders/*"
],
},

This allows us to alter our imports in the routing of the application :

Angular routing to library secondary entrypoint

And of course, the build reflects these settings :

Differential build of Angular library

Which means that now, only the dedicated bundle will rebuild when working on the components of this specific feature.

Access secondary entrypoint code

Now, let’s add some code to our feature, and monitor the effect on the build. For a start, we add a simple dto interface in ‘bills’ feature, and create a Bill in our application.

import { Bill } from 'orders/bills';

@Component({
...
})
export class AppComponent {
title = 'Orders Application';
bill: Bill = { id: '1', name: 'Archery supplies', amount: 123.45,
date: new Date(),
};
}

No effect on the build and the lazy loading, that’s fortunate because interfaces are not supposed to be produced in the js bundle.

Let’s add some code to a bills service, for example, on the front page we want the number of pending bills and on the bills page, all the bills. So we need to export the service in our feature public-api file, and then use it in our app component :

Using Angular library service in application

But, as soon as you do that, you create adherence between the application and the library entrypoint. The original orders feature is untouched, but the code of the bills bundle is now back in the main bundle :

That means that we don’t want to expose any code out of the feature if we want to keep it lazy loaded, so maybe we will refactor our library and have a core entrypoint in the library and separate secondary entrypoints for our features. This way we won’t confuse what is to be used by the application.

Refactoring Angular library for secondary entrypoints

Now, the library is called core , not that it’s very clever, but that allows us to create 2 separate entrypoints dedicated to our lazy loaded features. The build reflects the refactoring with separate bundles.

Angular secondary entrypoints for routing

In the next article, we will update to Angular 17 and move to the latest builder and see how optimized it will be.

Next part

Reference

Find all the steps for this article in the commits of the repo :

--

--