DEV Community

loading...

Manually Lazy load Components in Angular 8

Mayank Dubey
Curious coder.
・5 min read

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

Discussion (11)

Collapse
benifreitag profile image
Benjamin Freitag • Edited

Do you know how to get it to work with a Material Dialog instead of a viewcontainer, like this?

this.lazyLoader.load("lazy").then(res => {
      this.dialog.open(res.componentType)
    })

I extended your sample at stackblitz.com/edit/lazy-angular-d... but also recieve Error: No component factory found for LazyComponent. Did you add it to @NgModule.entryComponents?

Collapse
binarysort profile image
Mayank Dubey Author

Hi,

Have you tried providing alternate componentFactoryResolver in material dialog config?

Collapse
benifreitag profile image
Benjamin Freitag • Edited

Thanks. Yes, I tried this but it still causes the same error in Angular 8. I updated the Stackblitz.

In Angular 9 this is fixed and there's no need to pass componentFactoryResolver 👏

Thread Thread
binarysort profile image
Mayank Dubey Author

I think it's related to this issue github.com/angular/angular/issues/...

Collapse
dileep3605 profile image
Dileep3605

am trying your solution everything was running but it's throwing error on this state

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

Error: No component factory found for SiteedittoolModule. Did you add it to @NgModule.entryComponents?

if can anyone help me that will be great

Collapse
binarysort profile image
Mayank Dubey Author

Hi,

Can you share more code?

Collapse
fasidongit profile image
Kader Mohideen Fasid

How do you write tests for dynamic import() in angular?

Collapse
fuhrermani profile image
fuhrermani

how to pass inputs and catch outputs from this component.

Collapse
binarysort profile image
Mayank Dubey Author • Edited

The createComponent() method returns a reference to the loaded component. Use that reference to interact with the component by assigning to its properties or calling its methods.

Add input and output in component:-

@Input()
data: string;

@Output()
event = new EventEmitter();

In the service just add:-

const compRef = container.createComponent(compFactory);

(compRef.instance as LazyComponent).data = 'this is data';
(compRef.instance as LazyComponent).event.subscribe(v => ... );

Read more about dynamic components here: angular.io/guide/dynamic-component...

Collapse
binarysort profile image
Mayank Dubey Author

Some comments have been hidden by the post's author - find out more