Lazy loading Angular modules with Ivy
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:

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:

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:

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;
//...
}
//...
}
As we can see, we have several functions, which can be split in two categories: compileModuleAsync
and compileModuleSync
, each of which have two variants, suffixed with either __PRE_R3__
or __POST_R3__
. “R3” comes from “Renderer3”, which is the Ivy renderer. So this must mean that when using Ivy, the actual compileModuleAsync
and compileModuleSync
methods of the compiler are mapped to the ones suffixed __POST_R3__
. Indeed, if we look in the code, that is the case. If we were to disable Ivy and run or build again, then we would see the Compiler_compileModuleAsync
and Compiler_compileModuleSync
to be mapped to the __PRE_R3__
variants.
But for now let’s focus on what the compiler actually does when using Ivy. Well, first of all, we can see that the “async” method just calls the “sync” method and returns an already resolved promise with the result. And the compileModuleSync
method just returns a new NgModuleFactory
. Huh, not much compiling going on there, is it?
So what happens if we’re not using Ivy or if we disable AOT compilation? There are four cases and we can summarize them in a table:

I think going in depth for all of the four cases is too much for this article, but let me just give a few more details. Let’s start with Ivy disabled, which was how things were until recently. When using JIT compilation, Angular actually injects the JitCompiler
which does its magic on the fly, which is what you would expect. On the other hand, when using AOT compilation, the compiler will throw an error (as we can deduce from the code we debugged above). This is also expected, since you really don’t need a compiler, because compilation happens at build time. And the compiled output is actually an NgModuleFactory
². So, the dynamic import doesn’t return a Type
like it does in our current code, but instead it returns the NgModuleFactory
.
We’ve already seen what happens in the now default case of Ivy with AOT compilation enabled. What happens when we disable AOT? Well, turns out the Compiler
actually does the same thing. Remember that this relies on the fact that the NgModule
contains the factory built-in. When using AOT, this happens at build time and we can see it in the compiled output. But when using JIT, it’s not there. So what happens? Well, it turns out that in this case, the NgModule
decorator adds the needed metadata at runtime. This happens when the decorator is executed, which is when the module is loaded by the dynamic import.
So, while it might seem weird that with Ivy the compiler doesn’t seem to do much compiling, it’s actually a neat trick to have the lazy loading of the router code be exactly the same as it has been up to now.
And, look! The compiler also has a couple of compileModulesAndComponents
methods. What this gives us, besides the NgModuleFactory
is a list of component factories for all the components declared by the module. So we don’t even need to know the component’s type. We have the factory directly and we can create the component. That is kind of cool. As an example we can do a sort of Angular module explorer, that lazy loads different modules and allows you to instantiate each component. The ComponentFactory
even has information about inputs and outputs. You can check out a proof of concept in this GitHub repo, which also includes the stuff presented in this article. To see a running demo, checkout the StackBlitz demo below.
However, there are some problems with this. It will not work when building with the production flag. The production build runs an optimizer that strips the needed information. You could turn off AOT (check the angular.json
configuration) and in this case the module and components will be compiled and runtime. But still, when built for production, the names of the components will be minified.
Hope you not only learned how lazily load modules with Angular 9, but that you also understand how it works underneath.
[1] Actually this code is unchanged for years. As you find out from the rest of the article, its the underlying things that changed.
[2] This actually doesn’t happen when using the dynamic import as we did. It seems that Angular knows that it has to create a NgModuleFactory
only if the module is loaded as you would when using the router. So, to trick it, you have to create an object similar to the Routes
object you would create, even you still call the dynamic import function yourself.