Maximizing Performance with Lazy Loading and Preload Routing in Angular

Optimizing Your Angular App with Lazy Loading and Preload Routing Strategies

When we build an Angular application with multiple modules in a large app, the main script file becomes a giant monster. One alternative to improve the user experience is to use the Lazy Module.

Today, We learn how to use lazy loading to enhance the user experience and add preload and custom strategies for loading modules.

The Scenario

I'm working on a payment app with four modules Dashboard, MoneyTransfer, Wallet and Activity. Each one represents a business context with components. The following example shows the ActivityModule is the same for every module.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivityComponent } from './activity.component';
import { ActivityRouterModule } from './activity.routing.module';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: [ActivityComponent]
})
export class ActivityModule { }

Import every module in the app.module and register in the imports section. We need to configure the Router module to navigate to every component.

The RouterModule.forRoot use a route configuration with the path and the component to load in the <router-outlet></router-outlet>.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NotFoundComponent } from './not-found/not-found.component';
import {MoneyTransferModule} from "./moneytransfer/moneytransfer.module";
import {DashboardModule} from "./dashboard/dashboard.module";
import {WalletModule} from "./wallet/wallet.module";
import {ActivityModule} from "./activity/activity.module";
import {RouterModule, Routes} from "@angular/router";
import {WalletComponent} from "./wallet/wallet.component";
import {ActivityComponent} from "./activity/activity.component";
import {DashboardComponent} from "./dashboard/dashboard.component";
import {MoneyTransferComponent} from "./moneytransfer/moneytransfer.component";

const routes: Routes = [
    { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
    { path: 'wallet', component: WalletComponent },
    { path: 'activity', component: ActivityComponent },
    { path: 'dashboard', component: DashboardComponent },
    { path: 'money', component: MoneyTransferComponent },
    { path: '**', component: NotFoundComponent },
]
@NgModule({
  declarations: [AppComponent, NotFoundComponent],
  imports: [BrowserModule, MoneyTransferModule, DashboardModule, WalletModule, ActivityModule,
    RouterModule.forRoot(routes)
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

The app is ready to use. Let's see the final output.

Learn more about Routing in Angular

routing-1.png

What is Angular doing?

Every module is compiled, merged, packaged, and bundled in the main.js file. We are loading all modules in a single file, it hits the application load time, and the user needs to wait for all modules to interact with the application.

Some questions come to my head.

  • We are loading 36.0kB in the Dashboard area.
  • Why load all modules?
  • Why do we need to wait to load other modules that are not working?

We are going to improve the situation using Angular Lazy Loading Modules.

Lazy Loading

Let's start splitting the responsibility and separate importing all modules from the app.module. Create a new file app.routing.module, responsible for loading the modules.

The app.routing.module, help us to define the routes and load the modules, using the `loadChildren' property in the route definition.

The loadChildren import module when the user requests the routes it loads the module.

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },

  {
    path: 'wallet',
    loadChildren: () =>
      import('./wallet/wallet.module').then((m) => m.WalletModule),
  },
  {
    path: 'activity',
    loadChildren: () =>
      import('./activity/activity.module').then((m) => m.ActivityModule),
  },
  {
    path: 'dashboard',
    loadChildren: () =>
      import('./dashboard/dashboard.module').then((m) => m.DashboardModule),
  },
  {
    path: 'money',
    loadChildren: () =>
      import('./moneytransfer/moneytransfer.module').then(
        (m) => m.MoneyTransferModule
      ),
  },
  { path: '**', component: NotFoundComponent },
];

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

Next, we need to configure the modules to expose a RouterModule configuration to load the component using the RouterModule.forChild() method.

We do the same in every module, the example shows for ActivityModule

Configuring Lazy Loading

The ActivityRouterModule imports the RouterModuleand configures the RouterModule.forChild() with the routes object with empty path to load the component.

The ActivityRouterModule

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ActivityComponent } from './activity.component';

const routes = [  { path: '', component: ActivityComponent }]

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

The ActivityModule imports the ActivityRouterModule to provide the routing configuration.

We remove all module references from the app.modules and only need to register the app.routing.module.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { NotFoundComponent } from './not-found/not-found.component';
import {AppRoutingModule} from "./app-routing.module";

@NgModule({
  declarations: [AppComponent, NotFoundComponent],
  imports: [
    AppRoutingModule,
      BrowserModule
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Perfect! We improve the load, only getting the minimum files and every module load when the user navigates the path.

routing-2.png

What do we get?

  • The size of main.js is decreased from 32KB to 12.8 KB
  • The modules are loaded on demand.

Side effects

One of the advantages is our new pain, "the loading on demand"; when the user clicks on the module, it takes time to request and load the module, getting delayed.

image-20221020181917817.png

PreLoad Strategies

One solution for the side efect is one once to load the app is loaded, fetch all the remaining module chunks, and have faster navigation between different modules. We can get it, using the preloadingStrategy.

The Angular provides have two strategies PreloadAllModules and NoPreloading.

The NoPreloading is default behavior , when we dont set a preloadingStrategy . It disables the module preloading in all modules.

The PreloadAllModules its will preload all the lazy loaded modules in the app, and download the lazy modules modules asynchronously once you add the configuration in the root module routing.

The router in the forRoot method passes an object with the option preloadingStrategy: PreloadAllModules.

The PreloadAllModules strategies come from the angular router.

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

Now, we have the lazy loading and downloading of the remaining modules asynchrono

routing-3.png

Read more about PreLoadStrategy

Custom Loading Strategy

The PreloadAllModules loads all modules. Maybe we want to control which modules load, and maybe we want to control or be selective about which modules will use the preloadingStrategy.

We can customize the preloadingStrategy to create a class that implements the built-in PreloadingStrategy interface.

The class must implement the method preload(). In this method, we determine whether to preload the module or not in the route.

We need to activate some flags in the route to know if the route will load or not. The easy way is to use the route.data property to set up the flag.

The load function compares if it contains the preload option to load the modules, then execute the load or returns an empty observable.

import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({
  providedIn: 'root',
})
export class ModuleLoadingStrategyService implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data['preload']) {
      return load();
    }
    return of(null);
  }
}

Change the PreLoadAllModules to ModuleLoadingStrategyServicestrategy.

import {ModuleLoadingStrategyService} from "./config/module.loading.strategy";

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
        preloadingStrategy: ModuleLoadingStrategyService,
    })
  ],
  exports: [RouterModule],
})

Finally, set the preload property in the router.data for the module to use the custom preload strategy, for example, wallet.

{
    path: 'wallet',
    loadChildren: () =>
      import('./wallet/wallet.module').then((m) => m.WalletModule),
    data: { preload: true },
  },

routing-4.png

Perfect! We have lazy loading with a custom preload strategy to load specific modules in the app!

Recap

We learn how to implement lazy loading, preload the modules, and create a custom strategy to have a particular module for loading and speeding the application.

My recommendation use lazy loading with a custom preload strategy. Pick the modules necessary for the application or high demand by the users, and maybe add some delay in the loading time. I hope it helps to speed up the performance of your Angular Apps.