Lazy Loading Angular Libraries
Angular is a platform for building applications in TypeScript, HTML and CSS.
The basic building blocks of an Angular application are NgModules. An Angular app is typically comprised of a root module (that is responsible for bootstrapping) and one or more feature modules.
By default, NgModules are eagerly loaded, which means that as soon as the app loads, so do all the NgModules. However, Angular also provides support for lazy-loading NgModules. Lazy loading helps keep initial bundle sizes smaller, which in turn helps decrease load times.
Angular Libraries
Many applications need to solve similar problems so software engineer's like to craft generic solutions (for a particular domain) that can be re-used by other applications.
Angular Material (for example) is an Angular library that provides a comprehensive suite of UI components suitable for desktop, tablet and mobile devices.
Getting Started
I used the Angular CLI to generate the scafolding for each of my project's libraries, for example:
ng generate library sales
The CLI will create a new folder (/sales
) in your workspace's /projects
directory:
├── /serendipity
└── /e2e
└── /node_modules
└── /projects
└── /sales
└── /src
└── /app
└── /core
└── /assets
└── /environments
└── /themes
├── index.html
...
├── angular.json
├── package.json
├── tsconfig.json
...
Using your library
You don't have to publish your library in order to use it in your own app, but you do have to build it:
ng build sales
And, import from the library (in your core module) by name:
import { SalesModule } from 'sales';
Lazy loading your library
Let's start by creating a new folder (/lazy-loading
) in our workspace's /src/app
directory:
├── /serendipity
└── /src
└── /app
└── /core
└── /lazy-loading
├── sales-lib-wrapper.module.ts
└── /shared
├── app.component.html
├── app.component.sccc
├── app.component.ts
├── app.module.ts
├── app-routing.module.ts
└── /assets
└── /environments
└── /themes
├── index.html
...
Then all we need to do is create a simple wrapper module (sales-lib-wrapper.module.ts) for our library:
import { NgModule } from '@angular/core';
import { SalesModule } from 'sales';
@NgModule({
imports: [ SalesModule ]
})
export class SalesLibWrapperModule {}
We also need to update our App routing module as follows:
...
const routes: Routes = [
{
path: 'sales',
loadChildren: './lazy-loading/sales-lib-wrapper.module#SalesLibWrapperModule'
}
...
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {onSameUrlNavigation: 'reload'})
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}
Now, that we are lazy loading the Sales module (in the App routing module), every route in the Sales library routing module is a child route:
...
const routes: Routes = [
{
// path: 'sales/contacts',
path: 'contacts',
component: ContactsComponent,
canActivate: [AuthGuard],
runGuardsAndResolvers: 'always'
}
...
];
@NgModule({
imports: [ RouterModule.forChild(routes) ],
exports: [ RouterModule ]
})
export class LazyLibRoutingModule {}
And we no longer need to (statically) import the SalesModule (in the core module):
// import { SalesModule } from 'sales';
Measurement
To help you visualise bundle sizes you can use the Webpack Bundle Analyzer.
For example ("target": "es5"):
ng build --prod --named-chunks --stats-json && ./node_modules/webpack-bundle-analyzer/lib/bin/analyzer.js ./dist/serendipity/stats.json
For example ("target": "es2015"):
ng build --prod --named-chunks --stats-json && ./node_modules/webpack-bundle-analyzer/lib/bin/analyzer.js ./dist/serendipity/stats-es2015.json
You can use Chrome DevTools Network panel to inspect the properties of individual resources:
Afterword
If you are lazy loading a library that relies on another libraries entryComponents then you need to ensure that its (entry component-related) services are providedIn the library (module), for example:
import { Injectable } from '@angular/core';
import { MatDialog, MatDialogRef, MatDialogConfig } from '@angular/material/dialog';
import { ComponentType } from '@angular/cdk/portal';
import { AlertDialogComponent } from '../../components/dialogs/alert-dialog/alert-dialog.component';
import { ConfirmDialogComponent } from '../../components/dialogs/confirm-dialog/confirm-dialog.component';
import { SerendipityComponentsModule } from '../../serendipity-components.module';
...
@Injectable({
// providedIn: 'root'
providedIn: SerendipityComponentsModule
})
export class DialogService {
...
}
Source Code:
- GitHub: Serendipity
References:
- Angular Issues: Entry Components of a Lazy Loaded NgModule are not available outside the Module
- Medium: Why And How To Lazy Load Angular Libraries
- Angular in Depth blog: Automatically upgrade lazy-loaded Angular modules
- ITNEXT: Lazy loading can do that?
- Angular.io: Differential Loading
- Medium: The Need for Speed: Lazy Load Non-Routable Modules in Angular
- Christianly Demann's blog: The Complete Guide to Angular Load Time Optimization