Lazy Load Non-Routable Modules in Angular - The Need for Speed

As you probably know, Angular comes with a functionality that allows you to lazy load routable modules out of the box. Although in most cases this functionality is sufficient, it’s still only a small piece of the pie. What about situations where we want to lazy load modules that aren’t routable?

We can study one real-world case like this from our application. We have a page that displays a list of widgets. Each widget comes with a settings button that, when clicked, opens a side panel and allows the user to customize the widget’ settings.

The widget settings panel contains a world of components, directives, pipes, and providers. It would be a waste to add the module to our main bundle, because then all clients would have to download and parse it, even though most would probably never use it.

A better decision would be to lazy load the module when the user clicks on the edit settings button.

Let’s learn how can we do that.

First, we need to create the module:

@NgModule({
  declarations: [WidgetSettingsComponent, ...],
  providers: [...],
  imports: [CommonModule]
})
export class WidgetSettingsModule {
}

We created a WidgetSettingsModule and provided it with everything it needs (i.e., components, directives, etc.).

Our next step is to instruct Angular to ask Webpack to create a separate chunk for our module so we can lazy load it later on. We can do this by adding the module path to the angular.json file:

{
  "projects": {
    "your-app-name": {
      "architect": {
        "build": {
          "options": {
            "lazyModules": [
              "src/app/widget-settings/widget-settings.module"
            ]
          }
        }
      }
    }
  }
}

Great, now we can see that Webpack extracts our module, with all of its dependencies, to a separate chunk.

This is image title

Next, we need “something” that knows how to load our modules. We can learn from the router itself and use the SystemJsNgModuleLoader loader. Let’s declare it in our AppModule:

import { NgModule, SystemJsNgModuleLoader, NgModuleFactoryLoader } from '@angular/core';

@NgModule({
  providers: [
    { provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

To load the module, we’ll use a custom directive that takes the module name as input, then loads the module and injects the root component to the current ViewContainerRef. Here’s an example of the final result:

// panel.component.html
<ng-container loadModule="widgetSettings"></ng-container>

First, let’s create a provider that holds the lazy load modules map:

import { InjectionToken } from '@angular/core';

export type LAZY_MODULES = {
  widgetSettings: string;
};

export const lazyMap: LAZY_MODULES = {
  widgetSettings: 'src/app/widget-settings/widget-settings.module#WidgetSettingsModule'
};

export const LAZY_MODULES_MAP = new InjectionToken('LAZY_MODULES_MAP', {
  factory: () => lazyMap
});

Now, we can build the final piece in the puzzle — the directive:

@Directive({
  selector: '[loadModule]'
})
export class LoadModuleDirective implements OnInit, OnDestroy {
  @Input('loadModule') moduleName: keyof LAZY_MODULES;
  private moduleRef: NgModuleRef<any>;

  constructor(
    private vcr: ViewContainerRef,
    private injector: Injector,
    private loader: NgModuleFactoryLoader,
    @Inject(LAZY_MODULES_MAP) private modulesMap
  ) {}

  ngOnInit() {
    this.loader
      .load(this.modulesMap[this.moduleName])
      .then((moduleFactory: NgModuleFactory<any>) => {
        this.moduleRef = moduleFactory.create(this.injector);
      });

  }
}

We inject the loader provider and call the load() method passing the module path that we grab from the LAZY_MODULES_MAP injection token.

If we take a look at the source code we’ll see that the load() method uses [SystemJS](https://github.com/systemjs/systemjs) to load and compile the module. Then, it returns the result, an object of type NgModuleFactory.

A bit of advice — try to always understand how things work under the hood. Yes, you likely won’t completely understand everything, but still, it’s just JavaScript, so you should get a general sense of what’s going on.

Ok, we have a module factory, now we need to create an instance of it. We call the create() method passing the current injector, and we get a reference to the module.

At this stage, we have a module, but we still need to create the root component and inject it to the view. How do we do that?

Our first option would be to add it to the module’s bootstrap property:

@NgModule({
  declarations: [WidgetSettingsComponent],
  bootstrap: [WidgetSettingsComponent]
})
export class WidgetSettingsModule {
}

And then get it from the moduleRef:

ngOnInit() {
  this.loader
    .load(this.modulesMap[this.moduleName])
    .then((moduleFactory: NgModuleFactory<any>) => {
      this.moduleRef = moduleFactory.create(this.injector);
      const rootComponent = this.moduleRef._bootstrapComponents[0] // WidgetSettingsComponent
    });
}

But I don’t like to use a private API, as it might be changed. I mean, there is a reason it’s private, right? Instead, I recommend storing the root component in a static property on the module itself:

@NgModule({
  declarations: [WidgetSettingsModule.rootComponent],
  entryComponents: [WidgetSettingsModule.rootComponent]
})
export class WidgetSettingsModule {
  static rootComponent = WidgetSettingsComponent;
}

And get it from the moduleFactory:

type ModuleWithRoot = Type<any> & { rootComponent: Type<any> };

ngOnInit() {
  this.loader
    .load(this.modulesMap[this.moduleName])
    .then((moduleFactory: NgModuleFactory<any>) => {
      this.moduleRef = moduleFactory.create(this.injector);
      const rootComponent = (moduleFactory.moduleType as ModuleWithRoot).rootComponent;
    });
}

Finally, let’s get the component factory and create the component:

ngOnInit() {
  this.loader
    .load(this.modulesMap[this.moduleName])
    .then((moduleFactory: NgModuleFactory<any>) => {
      this.moduleRef = moduleFactory.create(this.injector);
      const rootComponent = (moduleFactory.moduleType as ModuleWithRoot)
        .rootComponent;
      
      const factory = this.moduleRef.componentFactoryResolver.resolveComponentFactory(
        rootComponent
      );
      
      this.vcr.createComponent(factory);
    });
}

And let’s not forget to destroy the module:

ngOnDestroy() {
  this.moduleRef && this.moduleRef.destroy();
}

And really, that’s all there is to it.

Here’s a live AOT example:

Thank you! Happy coding!

#Angular #JavaScript #Web Development

Lazy Load Non-Routable Modules in Angular - The Need for Speed
1 Likes20.95 GEEK