Ivy and the New Way of Lazy Loading Angular Modules
Ivy brings a compelling feature to the table: the ability to lazy load components without the need for an NgModule. Numerous articles cover this topic, and the standard approach looks 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);
}
}
The dynamic import statement is utilized to defer loading the component's code. Following that, a ComponentFactoryResolver is used to acquire a ComponentFactory for the target component. This factory is then handed over to a ViewContainerRef, which handles the DOM manipulation needed to insert the component.
However, a component rarely exists in complete isolation. Even simple components typically depend on utilities from other Angular modules. For instance, consider a lazy component that relies on the built-in ngFor directive:
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 root AppModule imports BrowserModule, which in turn exports ngFor, the lazily loaded component is unaware of it. Consequently, we encounter the following error, and the list fails to render:

The resolution involves creating an NgModule that declares our component and imports CommonModule, much like the traditional pattern. The key difference is that this module doesn't require any explicit action from us. It can reside in the same file as the LazyComponent, without even being exported, and everything functions as expected!
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 clever enough to inspect the NgModule and deduce that it must reference the ngFor directive from the @angular/common package.
But what if your goal is to lazy load an entire Angular module as well?
What's the motivation for that? One key reason is accessing its providers. Let's imagine the component we discussed requires a service that the module provides.
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 { }
Executing the code at this stage results in an error indicating that Angular cannot find a provider for that service. Note that in the current code, Angular treats LazyModule purely as metadata—an index card of sorts—that defines the component's dependencies. It doesn't instantiate the module, meaning its providers are never set up, which is exactly why the error occurs.
Therefore, we need to both load and instantiate the module. But how can this be achieved? If we export the NgModule, we can access it via the dynamic import. Yet, that only gives us the Type, not an instance. We might be tempted to simply use new on it, but we can't be sure that's sufficient or that Angular doesn't perform additional magic behind the scenes.
Let's take a step back. How is it that in Ivy we can directly instantiate components? This is because when Ivy compiles components, it embeds all the necessary instantiation logic directly within the class. After building our project, Angular generates a chunk containing our component. Here's a look at what it produces:

Compiled component
Did you notice? The factory is right there in the middle, responsible for instantiating the component. Actually, Angular should apply the same treatment to modules, pipes, directives, and services. Let's take a look:

Compiled Angular module
And there it is—a factory for the module, tucked within the injector definition. But how do we invoke that factory? It's clearly internal and not intended for direct calls. There has to be something in Angular that can handle this.
How does this work for components? Looking at our loadComponent method, we see that we inject a ComponentFactoryResolver, which presumably retrieves the factory from the internal definition we saw in the compiled output. We then pass this factory to the createComponent method of ViewContainerRef, which utilizes it to instantiate the component.
Surely there's an analogous mechanism for Angular modules, right? How do we find out?
Let's first recall the pre-Ivy approach:
@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);
}
}
Previously, creating a component required first creating a module. To build a module, we needed a module factory, which we obtained from the NgModuleFactoryLoader using SystemJsNgModuleLoader—now deprecated. Is there a replacement in place?
Let's think back. How did we even discover this method? The Angular router handles lazy loading, and by observing its implementation, we borrowed the same pattern. So, let's examine how Angular 9 handles it:
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));
}
}));
}
}
There it is—with a method name that hints at its purpose¹. The first branch deals with the old deprecated approach where loadChildren was a string, handled by the NgModuleFactoryLoader. We can ignore that. Today, loadChildren is a function that triggers a dynamic import and returns a module. That's the else branch. Let's adopt that pattern 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);
}
}
}
The ComponentFactoryResolver is no longer needed because the NgModulRef provides its own, but we now inject the compiler and injector.
And it functions! There's one caveat though. Using compiler in our code requires adding @angular/compiler to our bundle, which noticeably increases its size.
You're still reading, which is great. It implies you're not the kind of developer who copies code without understanding the underlying mechanisms.
Honestly, when I first encountered this code, I doubted it was the right solution. Compile? Why would I want to compile? Ivy defaults to ahead-of-time (AOT) compilation, meaning everything is already compiled. I definitely didn't want to do this—it sounded like a time sink. So, I went hunting for alternatives and ended up wasting time because I avoided examining what the compiler truly does. After all, compilers are complex and hard to understand, aren't they?
Let's not shy away. We'll debug and step into the method to see its inner workings:

Compiler debug
Here it is, cleaned up and without 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;
//...
}
//...
}
We see several functions that fall into two categories: compileModuleAsync and compileModuleSync, each with two variants suffixed __PRE_R3__ or __POST_R3__. "R3" is short for "Renderer3", the Ivy renderer. This suggests that with Ivy, the compiler's compileModuleAsync and compileModuleSync methods map to the __POST_R3__ variants. That's indeed what the code shows. If Ivy is disabled, we'd see Compiler_compileModuleAsync and Compiler_compileModuleSync mapped to the __PRE_R3__ versions.
For now, let's focus on what the compiler does under Ivy. The "async" method simply calls the "sync" method and returns an already-resolved promise with the result. The compileModuleSync method just returns a new NgModuleFactory. Not much compiling happening there, is there?
What happens without Ivy or with AOT disabled? We have four scenarios, which we can outline in a table:

NgModule compilation table
Delving deeply into all four cases would be too extensive for this article, but let me offer some additional clarity. Starting with Ivy disabled—the status quo until recently—JIT compilation causes Angular to inject the JitCompiler, which performs its magic on the fly. Conversely, with AOT compilation, the compiler throws an error, as we saw in the debug output. That's expected since compiling at build time makes a runtime compiler unnecessary. The compiled artifact is actually an NgModuleFactory². So, the dynamic import doesn't return a Type as it does in our current code; instead, it yields the NgModuleFactory.
We've already covered the default Ivy case with AOT enabled. What if we disable AOT? Interestingly, the Compiler behaves the same way. This relies on the fact that the NgModule has its factory built-in. With AOT, this happens at build time, visible in the compiled output. With JIT, it's not present at all. So what occurs? The NgModule decorator adds the necessary metadata at runtime. This transpires when the decorator executes, which happens as the module is loaded via dynamic import.
Thus, while it might seem odd that the Ivy compiler appears to do minimal work, it's actually a clever mechanism that ensures the router's lazy loading code remains uniform with previous versions.
And guess what? The compiler also exposes compileModulesAndComponents methods. These provide not just the NgModuleFactory, but also a list of component factories for every component the module declares. This means we don't even need to know the component's type upfront. The factory is right there, ready for component creation. That's pretty neat. For instance, we could build an Angular module explorer that lazy loads different modules and lets you instantiate each component. The ComponentFactory even carries details about inputs and outputs. A proof of concept is available in this GitHub repository, which includes all the examples from this article. A live demo is provided in the StackBlitz below.
However, these approaches have some drawbacks. Building with the production flag will break them. The production build's optimizer strips away the required information. You could disable AOT (check the angular.json configuration), allowing modules and components to be compiled at runtime. But even then, production builds minify component names.
I hope this gives you a clear understanding of how to lazy load modules in Angular 9 and what's happening behind the scenes.
[1] This code hasn't actually changed in years. As you'll discover in this article, it's the underlying infrastructure that has evolved.
[2] This doesn't happen when using our dynamic import method. It seems Angular only creates an NgModuleFactory when the module is loaded the way the router does it. To mimic this, you need to construct an object similar to the [Routes](https://github.com/angular/angular/issues/31886#issuecomment-516793641) object, even if you plan to call the dynamic import function yourself.
