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:

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

#angular #javascript #web-development #programming #developer

Lazy loading Angular Modules with Ivy
13.65 GEEK