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:

Can’t bind to ‘ngForOf’ since it isn’t a known property of ‘li’.

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;
		//...
	}
	//...
}

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:

`NgModule` compilation 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.