Angular Ivy makes it pretty easy to lazy load components, but what if we need to lazy load modules. Can we do that? In this article I’ll show you why you may need this and how it can be done.
One of the interesting features of Ivy is the ability to lazy load components, without requiring an NgModule
. There are lots of articles about this, and you basically do it like this:
import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<button (click)="loadComponent()">Load</button>
<ng-container #anchor></ng-container>
`
})
export class AppComponent {
@ViewChild('anchor', { read: ViewContainerRef }) anchor: ViewContainerRef;
constructor(private factoryResolver: ComponentFactoryResolver) { }
async loadComponent() {
const { LazyComponent } = await import('./lazy/lazy.component');
const factory = this.factoryResolver.resolveComponentFactory(LazyComponent);
this.anchor.createComponent(factory);
}
}<>
We use the dynamic import
statement to lazy load the component’s code. Then use a ComponentFactoryResolver
to obtain a ComponentFactory
for the component which we then pass to a ViewContainerRef
which modifies the DOM accordingly, by adding the component.
But usually you can’t have a component all by itself. Normally, you will need things from other Angular modules, even for simple components. Say for example, our lazy component uses the built-in ngFor
:
import { Component } from '@angular/core';
@Component({
selector: 'app-lazy',
template: `
This is a lazy component with an ngFor:
<ul><li *ngFor="let item of items">{{item}}</li></ul>`
})
export class LazyComponent {
items = ['Item 1', 'Item 2', 'Item 3'];
}<>
Even though our AppModule
imports BrowserModule
, which exports the ngFor
directive, because the component is loaded lazily it does not know about it, and we get the following error, and our list does not appear:
To solve this issue we need to create an NgModule
that declares our component and imports CommonModule
, just like we normally would. The difference is that we don’t have to do anything with this module. We can just add it to the same file as the LazyComponent
above, not even exporting it, and now everything just works!
import { Component, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-lazy',
template: `
This is a lazy component with an ngFor:
<ul><li *ngFor="let item of items">{{item}}</li></ul>`
})
export class LazyComponent {
items = ['Item 1', 'Item 2', 'Item 3'];
}
@NgModule({
declarations: [LazyComponent],
imports: [CommonModule]
})
class LazyModule { }<>
Angular is smart enough to analyze the NgModule
and see that it needs to reference the ngFor
directive from the @angular/common
package.
But what if you actually do want to lazy load an Angular module as well?
Why would you want that? Well, one reason is for its providers. Let’s say that the component above requires a service, that is provided by the module.
import { Component, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LazyService } from './lazy.service';
@Component({
selector: 'app-lazy',
template: `
This is a lazy component with an ngFor:
<ul><li *ngFor="let item of items">{{item}}</li></ul>
{{service.value}}`
})
export class LazyComponent {
items = ['Item 1', 'Item 2', 'Item 3'];
constructor(public service: LazyService) { }
}
@NgModule({
declarations: [LazyComponent],
imports: [CommonModule],
providers: [LazyService]
})
class LazyModule { }<>
Running the code at this point gives an error that Angular cannot find a provider for that service. Note that as the code is now, Angular just uses the LazyModule
as metadata, as a sort of index card which tells it what components need what. It does not instantiate it, which means it will not set up the providers, which results in the error we get.
So we need to load and instantiate the module too. But how do we do that? If we export the NgModule
, we can then access it from the dynamic import. But that is just the Type
, we need to instantiate it some how. We could maybe just “new” it up, but who knows if that is enough and if Angular doesn’t do something else besides that.
Let’s backtrack a little. How come in Ivy we can instantiate components directly? Well, it’s because when Ivy compiles components, it places everything it needs to instantiate it right there in the class. When we build our project, Angular creates a chunk which contains our component. Have a look at what it looks like:
Compiled component
See? There is the factory, right in the middle, which instantiates the component. In fact, Angular should do the same thing for modules, pipes, directive, services. Let’s have a look:
Compiled Angular module
Oh, there it is, a factory for the module, even if it is in the injector definition. But how do we call that factory? It’s obviously internal and not meant to be called directly by us. There must be something in Angular that can do this.
How does it work for components? Well, if we look at our loadComponent
method we can see that we inject a ComponentFactoryResolver
which presumably obtains the factory from that internal definition we saw in the compiled code above. Then we pass the component factory to ViewContainerRef
’s createComponent
method, which uses it to instantiate the component.
There should be something similar for Angular modules, right? How can we find out?
Well… First let’s remember how we did things before Ivy:
@Component({
selector: 'app-root',
templateUrl: 'app.component.html'
providers: [
{ provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }
]
})
export class AppComponent {
constructor(private injector: Injector,
private loader: NgModuleFactoryLoader) {
}
@ViewChild('anchor', { read: ViewContainerRef }) anchor: ViewContainerRef;
loadComponent() {
const moduleFactory = this.loader.load('lazy/lazy.module#LazyModule');
const moduleRef = moduleFactory.create(this.injector);
const cmpFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(AComponent);
this.anchor.createComponent(factory);
}
}<>
We had to first create a module before we could create a component. To create a module, we needed a module factory, which we got directly from the NgModuleFactoryLoader
using SystemJsNgModuleLoader
, which is now deprecated. So is there anything else in place?
Let’s think back again. How did we find out how to do this in the first place? Well, Angular does lazy loading via the router. And looking there, we saw what it did, and we did the same. So let’s have another look, at how it does it in Angular 9:
private loadModuleFactory(loadChildren: LoadChildren): Observable<NgModuleFactory<any>> {
if (typeof loadChildren === 'string') {
return from(this.loader.load(loadChildren));
} else {
return wrapIntoObservable(loadChildren()).pipe(mergeMap((t: any) => {
if (t instanceof NgModuleFactory) {
return of (t);
} else {
return from(this.compiler.compileModuleAsync(t));
}
}));
}
}<>
Ah, there it is, with a very suggestive method name¹. The first branch is for the old deprecated way, in which you specified loadChildren
as a string and the NgModuleFactoryLoader
did the magic. We don’t care about that. Nowadays, you specify loadChildren
as a function that calls a dynamic import and returns a module. That is treated in the else
branch. Let’s just copy-paste that and see if it works. Here’s our updated AppComponent
:
import { Compiler, Component, Injector, NgModuleFactory, ViewChild, ViewContainerRef } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<button (click)="loadComponent()">Load</button>
<ng-container #anchor></ng-container>
`
})
export class AppComponent {
@ViewChild('anchor', { read: ViewContainerRef }) anchor: ViewContainerRef;
constructor(private compiler: Compiler, private injector: Injector) { }
async loadComponent() {
const { LazyComponent, LazyModule } = await import('./lazy/lazy.component');
const moduleFactory = await this.loadModuleFactory(LazyModule);
const moduleRef = moduleFactory.create(this.injector);
const factory = moduleRef.componentFactoryResolver.resolveComponentFactory(LazyComponent);
this.anchor.createComponent(factory);
}
private async loadModuleFactory(t: any) {
if (t instanceof NgModuleFactory) {
return t;
} else {
return await this.compiler.compileModuleAsync(t);
}
}
}<>
We don’t need the ComponentFactoryResolver
anymore because the NgModulRef
comes with its own, but we now have to inject the compiler and injector.
And… it works! One caveat though. To use compiler
in our code, we need to add @angular/compiler
to our bundle which is quite a significant bundle size increase.
Well, you’re still here. I guess that means you’re not the type of developer that just copy-pastes and is satisfied that it just works, without understanding why or what’s actually going on.
To be honest, when I first saw this code, I thought it wasn’t the right solution. Compile? Why do I want to compile? Ivy defaults to ahead-of-time (AOT) compilation, so everything is already compiled. I certainly don’t want to be doing this. It sounds like something that would waste a lot of time. So, I went looking for something else and wasted a lot of time myself, because I was reluctant to look at what the compiler actually does. I mean, it’s a compiler. It’s certain to be complex and hard to understand, right?
Well, let’s debug and step into the method and see what it does:
Compiler debug
Here it is, cleaned up and without the comments:
function _throwError() {
throw new Error(`Runtime compiler is not loaded`);
}
const Compiler_compileModuleSync__PRE_R3__ = _throwError;
const Compiler_compileModuleSync__POST_R3__ = function (moduleType) {
return new NgModuleFactory$1(moduleType);
};
const Compiler_compileModuleSync = Compiler_compileModuleSync__POST_R3__;
const Compiler_compileModuleAsync__PRE_R3__ = _throwError;
const Compiler_compileModuleAsync__POST_R3__ = function (moduleType) {
return Promise.resolve(Compiler_compileModuleSync__POST_R3__(moduleType));
};
const Compiler_compileModuleAsync = Compiler_compileModuleAsync__POST_R3__;
// And a little lower we have this:
class Compiler {
constructor() {
this.compileModuleSync = Compiler_compileModuleSync;
this.compileModuleAsync = Compiler_compileModuleAsync;
//...
}
//...
}<>
#angular #javascript #web-development #programming #developer