Splitting your Angular app into several smaller bundles can make your site faster by reducing download and execution times. Instead of loading all code upfront it's fetched lazily when needed.
Most guides to lazy loading Angular modules use Angular's RouterModule
and the loadChildren
property to load code when the user first navigates to a certain page. But that means you can't lazy load code if whether the code is needed doesn't depend on a route change.
This article will explain how to lazy load Angular feature modules independently of the router.
Opting into Angular Ivy
To follow this guide you'll need to opt into Angular's new compilation and rendering pipeline, called Angular Ivy.
For a new project pass the --enable-ivy
flag to ng new
.
For existing projects, add this next to compilerOptions
in tsconfig.app.json
:
"angularCompilerOptions": {
"enableIvy": true
},
Getting set up
Ok, so we've got a project. Now we need a module and a component to lazy load.
ng generate module lazy
ng generate component lazy/my-component
To make my component bundle a little bigger I also installed moment.js with npm install moment --save
and then imported it at the top of my-component.component.ts
:
import * as moment from "moment"
console.log(moment) // Needed or else moment will be excluded from the bundle
We also want an easy way to get the component from the module, so we'll add a getMyComponent
function to lazy.module.ts
.
export class LazyModule {
static getMyComponent() {
return MyComponentComponent
}
}
Making lazy loading work
First, let's replace the contents of app.component.html
with a button to load the component and a placeholder where we can render the component:
<button (click)="showLazyComponent()">Show lazy loaded component</button>
<app-my-component #componentPlaceholder></app-my-component>
Most of the work needs to be done in app.component.ts
.
We use @ViewChild
to get a reference to the element that will contain the rendered component.
We'll need a ComponentFactoryResolver
instance to create the component later, so we modify the constructor to inject it into the component.
Finally, we've got the showLazyComponent
click event handler. We use an ES2015 import statement to load the LazyModule
, then generate a factory for the component class and render the component into the placeholder.
Here's what the file looks like after we've made all our changes:
import {
Component,
ComponentFactoryResolver,
ViewContainerRef,
ViewChild
} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
@ViewChild('componentPlaceholder', { read: ViewContainerRef, static: true })
public componentPlaceholder: ViewContainerRef;
constructor(private resolver: ComponentFactoryResolver) {}
showLazyComponent() {
import('./lazy/lazy.module').then(({ LazyModule }) => {
const MyComponent = LazyModule.getMyComponent();
const factory = this.resolver.resolveComponentFactory(MyComponent);
const ref = this.componentPlaceholder.createComponent(factory);
});
}
}
Angular now creates a separate bundle called lazy-lazy-modules.js
. It's only loaded when the "Show lazy loaded component" button is clicked.
Passing values to the component
Passing values to our component through the template doesn't work, so we need to do it directly using the instance ref.
Suppose our component has an input property called message
. Here's how we can pass that value to the instance:
ref.instance.message = "Hello"
Likewise, if we have an output property we can subscribe to the event emitter:
ref.instance.evt.subscribe(console.log);
Maintaining meaningful bundle names for the production build
If we run ng build --prod
now the file we split off will have a name like 5-es2015.d49b738d7e72bc78a00f.js
. But that doesn't tell us very much about what's being loaded.
If you add the --named-chunks
flag to the ng build
command you'll get a meaningful chunk name like lazy-lazy-module-es2015.6d182f0f840a62287af1.js
instead.
Having consistent bundle names also allows a tool like DebugBear to track the download size of your different bundles over time.