Log In

Angular Lazy Loading


by Yariv Katz

Our application needs to load fast. In fact google published on angular docs, that if our app takes more then 3 seconds to load (even on an old mobile phone with slow connection) we will lose more than 50% of our users. A professional web application needs to load fast, and as a result we have to make the user download the application files he needs and not all our code. Lets look at a simple example, say we have an app that contain the homescreen and a settings screen, our users are visiting the homescreen but they do not always go to the settings page, do we really need to load the entire code base of the settings every time the user visits our application? The idea is load what the user need, lazy load all the rest, so in this example load the homepage but only load the settings pages when the user wants to visit them. This is refered to lazy loading, and this article will be devoted to how it is done with angular 7.

Lazy loading with the Router

Let's first start by the example we talked before. We will create two modules, one containing the root module, the other one called SettingsModule that contains all the logic from the settings screens. We will like to load the settings module, only when the user goes to the settings page.

Lets start by creating a new angular project with angular cli.

> npx @angular/cli new lazy-loading-tutorial
> ? Would you like to add Angular routing? (y/N) y
> ? Which stylesheet format would you like to use? SCSS
> cd lazy-loading-tutorial

We created a new angular 7 project, and added SCSS styling and router to it. Let's create a new module for the settings:

> ng g m ../Settings --routing

this will create a settings folder and in it the file for our module. Adding the --routing flag will also add a routing module. Lets create a few screens in our settings module. We will want the route:
/settings/user - to lead to the user settings page
/settings/dashboard - to lead to the dashboard settings page Lets create those 2 components:

> ng g c ../settings/UserSettings
> ng g c ../settings/DashboardSettings

We want the settings module to be lazy loaded when the user goes to the settings urls. The settings urls all have a prefix route of /settings/, whether it's /settings/user or /settings/dashboard, they both start with /settings/ so we will load the settings routes through that common prefix. So on the SettingsModule routes we will specify the routes without the prefix and then connect them through the root route module to the /settings/ prefix. Lets start by modifying the settings-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { UserSettingsComponent } from './user-settings/user-settings.component';
import { DashboardSettingsComponent } from './dashboard-settings/dashboard-settings.component';

const routes: Routes = [
    {path: 'user', component: UserSettingsComponent},
    {path: 'dashboard', component: DashboardSettingsComponent},
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class SettingsRoutingModule { }

we simply loaded the path to the settings components without the common prefix. Now lets lazy load the settings module. Modify the app-routing.module.ts:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
{path: 'settings', loadChildren: '../settings/settings.module#SettingsModule'}
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

We added a route that points to our settings module through the loadChildren property of the path. loadChildren will get a string of a relative path (relative from the current route file) to the file where the SettingsModule is defined and will get a hashtag and after which the class name of the module. In the app.component.html add a link to the settings pages:

<nav>
<ul>
    <li>
    <a routerLink="/settings/user">
        User settings
    </a>
    </li>
    <li>
    <a routerLink="/settings/dashboard">
        Dashboard settings
    </a>
    </li>
</ul>
</nav>

<router-outlet></router-outlet>

We used to routerLink directive to link through the angular router so the page won't reload. Now activate the app with:

> ng serve

To verify that our lazy loading is working, open the network tab of developer tools, and filter by JS files. Load the app on the browser in the homepage and then go to one of the settings routes, only when you travel to the homepage will you see the settings module js file is loaded. First way to lazy loading is a success. We will use this way to achieve lazy loading throughout our routes that the user will not always go to on each run of our app.

Lazy loading component

There are times when we have some heavy duty components, but they do not belong to a lazy loaded route but rather they belong to the homepage. For example lets say we have a modal in our homepage the contains a lot of components inside and complicated logic. That modal will only be shown if the user press a certain button and not in every run of our app the user press the button and wants to see that modal. This means that it's better to make that modal lazy loaded. First we will create a module called Modals and create our lazy modal component in that module.

> ng g m ../Modals
> ng g c ../modals/LazyModal

In our app component template, we will add a button that when pressed will load our lazy modal. Modify the app.component.html

<nav>
    <ul>
        <li>
        <a routerLink="/settings/user">
            User settings
        </a>
        </li>
        <li>
        <a routerLink="/settings/dashboard">
            Dashboard settings
        </a>
        </li>
    </ul>
</nav>

<button (click)="loadLazyModal()">Click me for lazy modal</button>

<!-- dynamic component will be loaded here -->
<div #modalContainer>

</div>

<router-outlet></router-outlet>

As well as the button that when clicked will activate a function that will load the lazy modal, we also created a div container with a template reference, our modal will be placed in that div. Lets modify the app.component.ts

import { 
    Component, 
    ViewChild, 
    ViewContainerRef, 
    Compiler, 
    Injector, 
    ComponentFactory 
} from '@angular/core';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent {
    @ViewChild('modalContainer', {read: ViewContainerRef})
    modalContainer: ViewContainerRef;
    
    constructor(
        private _compiler: Compiler,
    ) {}

    async loadLazyModal() {
        const module = await import('../modals/modals.module');
        const compiled = await this._compiler.compileModuleAndAllComponentsAsync(module.ModalsModule);
        let factory = compiled.componentFactories.find((componentFactory: ComponentFactory) => componentFactory.selector === 'app-lazy-modal');
        if (factory) {
        this.modalContainer.createComponent(factory);      
        }
    }
}

We have a @ViewChild grabbing the ViewContainerRef of the div so we can create the lazy modal in the container we want. We have the method loadLazyModal that will be called when the user pressed the button, we made that method async so we can use await when loading the file. We are calling import as a function and passing a relative path to the module file. We are using the injected compiler so we can compile the module we loaded and all the components of that module. This will bring us ModuleWithComponentFactories after resolved and from that object we can grab the component factory we need. We are calling the ViewContainerRef create and passing the factory of the component to create our lazy modal. For this to work we need to also change the module config in the tsconfig and set it to esnext. Your tsconfig.json should look like this:

{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "sourceMap": true,
        "declaration": false,
        "module": "esnext",
        "moduleResolution": "node",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "target": "es5",
        "typeRoots": [
        "node_modules/@types"
        ],
        "lib": [
        "es2018",
        "dom"
        ]
    }
}

Now running our app again and pressing the button, you should see in the developer tools a js file loaded and our modal is shown.

When I first discovered I can lazy load entire angular modules, or even just js modules I wondered if i can use this technique to load my modules from remote origin and not from my local project. Unfortunately webpack is a file bundler and not a script loader, the dynamic import instructs webpack about the file that we want to dynamically load and it creates chunk from that file, webpack cannot deal with files that reside in an external URL.

It was also important for me to verify that this method will compatible with AOT compilation. try and run build with aot:

> ng build --aot

the build will complete successfully.

Lazy loading named router

Although using the router, we can still load module in a lazy way, and load the lazy component as part of the screen like we did before and without changing the url. And strangely enough we can do it again with our router. In your app.component.html create another button to load the lazy modal in another way. Also we will create below that button a named router outlet.

<nav>
    <ul>
        <li>
        <a routerLink="/settings/user">
            User settings
        </a>
        </li>
        <li>
        <a routerLink="/settings/dashboard">
            Dashboard settings
        </a>
        </li>
    </ul>
</nav>

<button (click)="loadLazyModal()">Click me for lazy modal</button>

<!-- dynamic component will be loaded here -->
<div #modalContainer>

</div>

<hr/>

<button (click)="loadLazyModal2()">Another lazy loading way</button>

<router-outlet name="modal"></router-outlet>

<router-outlet></router-outlet>

We created a named router outlet where we will load our lazy loaded component. Now lets modify the app routes app-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
    {path: 'settings', loadChildren: '../settings/settings.module#SettingsModule'},
    {path: 'modal', outlet: 'modal', loadChildren: '../modals/modals.module#ModalsModule'}
];

@NgModule({
    imports: [RouterModule.forRoot(routes)],
    exports: [RouterModule]
})
export class AppRoutingModule { }

We added another route for the named outlet, if the named outlet is getting the path modal then we will lazy load the routes defined for the ModalsModule. This means that we will need to add a routing module for the ModalsModule. add the file modals-routing.module.ts in the modals folder:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LazyModalComponent } from './lazy-modal/lazy-modal.component';

const routes: Routes = [
    {path: '', component: LazyModalComponent},
];

@NgModule({
    imports: [RouterModule.forChild(routes)],
    exports: [RouterModule]
})
export class ModalsRoutingModule { }

We will render the lazy modal on the empty path. We need to connect this module to the ModalsModule imports array. Lets implement the logic behind the button we added. Modify the file app.component.ts to this:

import { 
    Component, 
    ViewChild, 
    ViewContainerRef, 
    Compiler, 
    ComponentFactory 
} from '@angular/core';
import { Router } from '@angular/router';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent {
    @ViewChild('modalContainer', {read: ViewContainerRef})
    modalContainer: ViewContainerRef;
    
    constructor(
        private _compiler: Compiler,
        private _router: Router
    ) {}

    async loadLazyModal() {
        const module = await import('../modals/modals.module');
        const compiled = await this._compiler.compileModuleAndAllComponentsAsync(module.ModalsModule);
        let factory = compiled.componentFactories.find((componentFactory: ComponentFactory<any>) => componentFactory.selector === 'app-lazy-modal');
        if (factory) {
        this.modalContainer.createComponent(factory);      
        }
    }

    loadLazyModal2 = () => {
        this._router.navigate([{outlets: {modal: 'modal'}}], {skipLocationChange: true});
    }
}

When the user clicks the button we now added, we are loading to the named outlet the route modal that will lazy load our modal. We can also navigate without changing the url so there will be no weird named outlet url.

Summary

Splitting our app to lazy loaded chunks is a key ingredient to making our app load faster. We showed 3 ways here to lazy load you angular app. Mostly you will split your app based on the routing, but now and again you might want to lazy load a certain part of the screen and we showed 2 ways to do that. I guess now is the time to refactor your angular app and use lazy loading where you can. A quick platform to manage the dev team and the QA team is important feature from the bug reporting tool or kanban board you choose. Bugeez was created using the newest angular version with performance in mind, so we relay heavily on lazy loading of our app components to bring you the fastest dev management and bug reporting tool available. Happy Coding...