Manually Lazy load Components in Angular 8

Manually Lazy load Components in Angular 8

Angular is amazing! It is fast and smooth once loaded in the browser, but most often the start-up time suffers. Improving the start-up time for Angular apps is crucial for getting a high-quality app and a better user experience. Reducing the app's bundle size is one of the many ways to help with that, and that is where lazy loading comes into play. You must be familiar with lazy loading via Angular routes. In this article, we are going to explore how we can manually lazy load components.

What is Lazy Loading?

Lazy loading or "On-demand loading" is a programming practice where we delay the loading of an object until its needed. In simple terms, you put off doing something which is not required at the moment.

Why do we need lazy loading? Well, single-page applications tend to be fast once loaded but initial loading time often suffers. This is because of a huge amount of javascript that needs to be downloaded and interpreted by the browser to boot up the Javascript app. To deal with this we need to reduce the size of the main javascript bundle necessary to boot the app (for angular apps main.js). This can be achieved with lazy loading. We don't load unused bits (modules) and load them on demand.

For example, we have settings area in our app which users rarely visit. Until necessary, we do not want to load the JavaScript corresponding to the settings area. At the latest, when the user clicks on the corresponding menu item, we can fetch the Javascript file (module) and load up that section of our app.

For this to work, our compiled code for that settings area must be isolated in a dedicated Javascript file, so that we can fetch it at runtime when needed. This sounds complex. Thankfully, Angular provides this functionality and does all the file bundling for us.

Lazy Loading in Angular

Great, we know what lazy loading is, but how does it work in Angular? Or what can be lazy-loaded in Angular? As the title suggests, you might say a Component. However, that's not entirely possible.

In Angular, the most basic unit is a module. A module is a mechanism to group components, directives, pipes and services that are related. So, modules are necessary for Angular to know which dependencies are required and which components are used in the template. Therefore, the most basic unit that can be lazy-loaded in Angular is a module, and with the module come the bundled components that we are interested in.

The easiest way to lazy-load modules is via routes:

const routes: Routes = [
  {
    path: 'home',
    loadChildren: () => import('./home/home.module').then(m => m.HomeModule)
  },
 {
    path: 'settings',
    loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule)
  },
];

The loadChildren property takes a function that returns a promise using the browser's built-in syntax for lazy loading code using dynamic imports import('...').

But we want more control over lazy loading, and not just with routes. We want to open a dialog, and lazy load its containing component just when a user opens that specific dialog.

Manual Lazy Loading of Modules

Angular 8 uses the browser's built-in dynamic imports import('...').

Dynamic import() introduces a new function-like form of import that caters to those use cases. import(moduleSpecifier) returns a promise for the module namespace object of the requested module, which is created after fetching, instantiating, and evaluating all of the module’s dependencies, as well as the module itself.

In simple terms, dynamic import() enables async loading of JS Modules. So, we don't have to worry about creating a dedicated JS bundle for our module, dynamic import() feature will take care of it.

Let's do it. First, we will generate a module which we will load lazily:

ng g m lazy

then we will generate an entry component for lazy module:

ng g c --entryComponent=true --module=lazy lazy

Our lazy.module.ts file should look like this:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LazyComponent } from './lazy/lazy.component';


@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [LazyComponent],
  entryComponents: [LazyComponent]
})
export class LazyModule {
  // Define entry property to access entry component in loader service
  static entry = LazyComponent;
}

We need to define our lazy widgets:

import { NgModuleFactory, Type } from '@angular/core';

// This will create a dedicated JS bundle for lazy module
export const lazyWidgets: { path: string, loadChildren: () => Promise<NgModuleFactory<any> | Type<any>> }[] = [
  {
    path: 'lazy',
    loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
  }
];

// This function will work as a factory for injecting lazy widget array in the main module
export function lazyArrayToObj() {
  const result = {};
  for (const w of lazyWidgets) {
    result[w.path] = w.loadChildren;
  }
  return result;
}

Define the injection token to inject lazy widgets in the service:

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

export const LAZY_WIDGETS = new InjectionToken<{ [key: string]: string }>('LAZY_WIDGETS');

Create a LazyLoaderService:

import { Injectable, Injector, Compiler, Inject, NgModuleFactory, Type, ViewContainerRef } from '@angular/core';
import { LAZY_WIDGETS } from './tokens';

@Injectable({
  providedIn: 'root'
})
export class LazyLoaderService {

  constructor(
    private injector: Injector,
    private compiler: Compiler,
    @Inject(LAZY_WIDGETS) private lazyWidgets: { [key: string]: () => Promise<NgModuleFactory<any> | Type<any>> }
  ) { }

  async load(name: string, container: ViewContainerRef) {
    const tempModule = await this.lazyWidgets[name]();

    let moduleFactory;

    if (tempModule instanceof NgModuleFactory) {
      // For AOT
      moduleFactory = tempModule;
    } else {
      // For JIT
      moduleFactory = await this.compiler.compileModuleAsync(tempModule);
    }

    const entryComponent = (moduleFactory.moduleType as any).entry;
    const moduleRef = moduleFactory.create(this.injector);

    const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(entryComponent);

    container.createComponent(compFactory);
  }


}

LazyLoaderService loads the module with dynamic import(). If it's compiled ahead of time then we just take it as it is. If we are using JIT mode we have to compile it. After, we inject all the dependencies in the module. Now to load the component into view we have to instantiate the component dynamically. This can be done with componentFactoryResolver. After resolving the entry component we just load it inside the container.

Provide the widgets and LazyLoaderService in AppModule:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LazyLoaderService } from './lazy-loader.service';
import { LAZY_WIDGETS } from './tokens';
import { lazyArrayToObj } from './lazy-widgets';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [LazyLoaderService, { provide: LAZY_WIDGETS, useFactory: lazyArrayToObj }],
  bootstrap: [AppComponent]
})
export class AppModule { }

That's it, just call the LazyLoaderService and manually load LazyComponent into the view.

We've seen that lazy loading can help us a lot to optimize our Angular application. This process enables us to isolate the component we want to show in dialog or anywhere else and we don't need to bundle it up with the main module.

Here's a GitHub repo with the complete code: https://github.com/binary-sort/lazy-angular