Angular Routing Part 3: Lazy Loading

In the previous posts, we created the components for our book tracking application and added the routing to the application. We are now going to refactor our application to add lazy loading to it.

What is lazy loading?

It is necessary to load our components in the browser to render our application. How we have set up our application in the previous posts, we use eager loading, which is the default loading strategy for components in Angular. With this strategy, all the components registered in the appModule are loaded and then the page will be rendered. This means that when you have a large application, the initial loading of your application can take a long time.

To overcome this problem, Angular has also another loading strategy called lazy loading. With this strategy, the modules are only loaded when you first navigate to the route of that module. Initially, there are fewer components loaded, resulting in much smaller bundle sizes and an application that loads much faster.

Feature Modules

Okay, but what are those modules I’m talking about? For now, we only have one module in our application: the appModule. This is our root module of the application. As your application grows, it is a good practice to separate your application into feature modules based on the functionality in the application. The structure of a feature module is exactly the same as the root module: you decorate it with the @NgModule decorator. The only difference is that feature modules import CommonModule instead of BrowserModule to use common directives such as ngIf and ngFor. In our application, we could separate all the components related to books in a separate feature module.

Refactoring our application

Before we start refactoring our application, we are going to run the application to see what javascript modules are created.

And when we look in the network tab of our browser we see that the following is loaded when we first launch our application.

First, we are going to create the feature module for Books and their routing. Navigate to the books folder in the terminal and run the following commands:

ng generate module books --flat
ng generate module books-routing --flat

In the booksRoutingModule, we add the routes for all components related to books:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes } from '@angular/router';
import { BooksComponent } from './books.component';
import { BookDetailComponent } from './book-detail/book-detail.component';
import { BookEditComponent } from './book-edit/book-edit.component';

const routes: Routes = [
  {
    path: '',
    component: BooksComponent
  },
  {
    path: ':id',
    component: BookDetailComponent,
  },
  {
    path: ':id/edit',
    component: BookEditComponent,
  }
];

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

We import the BooksRoutingModule and the SharedModule that we created in the previous post in the BooksModule. In this SharedModule, we imported and exported the Angular Material Modules we need in our application. Then we need to declare the components related to books in this module.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BooksRoutingModule } from './books-routing.module';
import { SharedModule } from '../shared/shared.module';
import { BooksComponent } from './books.component';
import { BookDetailComponent } from './book-detail/book-detail.component';
import { BookEditComponent } from './book-edit/book-edit.component';

@NgModule({
  declarations: [
    BooksComponent,
    BookDetailComponent,
    BookEditComponent
  ],
  imports: [
    CommonModule,
    SharedModule,
    BooksRoutingModule
  ]
})
export class BooksModule { }

We then refactor our AppRoutingModule to lazy load these routes and delete all routes related to books and add the following route:

{ 
    path: 'books', 
    loadChildren: () => import('./books/books.module').then(m => m.BooksModule) 
  },

Here we specify that when we navigate to the route ‘books’, the BooksModule needs to be loaded.

In the AppModule, we delete the component declarations for the BooksComponent, BooksDetailComponent and BooksEditComponent.

When we now run our application, we see that there is also a JavaScript books-module created and the main module is slightly smaller.

In the network tab of the browser, we see that when we first launch the application the books-module is not loaded into the browser.

When we then click on “Your book list” or “Add new book”, the books-module is being loaded.

Preloading strategies

With this lazy-loading, the initial loading of our application can be a lot faster, especially when you have a large application. A downside of lazy loading is that the navigation can be slow when you need to load a new module (depending on the users’ network). We can solve this by the preloading strategies of Angular.

By default Angular doesn’t preload modules. You can easily preload modules by specifying the value property preloadingStrategy in the router configuration and set it to PreloadAllModules. With this strategy, after the application is rendered, it starts preloading all the other modules that are lazy loaded in the background.

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

This will make your application faster but is not always what you want. If you have a large application, a lot of unnecessary data may be loaded in the background. So we need to find a solution where we can preload only the core features, and the less used features are loaded on demand when the user navigates to a route in that module. We can do that by creating our own custom preloading strategy.

Creating a custom preloading strategy

To create a custom preloading strategy, Angular provides us with the PreloadingStrategy class which we need to implement in our own preloading strategy class and we need to override its preload method.

The CustomPreloadingStrategy class will look like this:

import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class CustomPreloadingStrategy implements PreloadingStrategy {

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if(route.data && route.data['preload']) {
      return load();
    } else {
      return of(null);
    }
  }
}

Next, we need to change our routing configuration and specify for each path that has loadChildren in its configuration if it needs to be preloaded. We can do that by adding the following to the route configuration:

data: { preload: true }

We only need to add this to the paths we want to preload because when you take a look at the preload method, we only return the load-method when a route has data and that data has a property preload with the value true. So when we don’t add this configuration to the route, this will not be preloaded.

The complete AppRoutingModule will look like this:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { PageNotFoundComponent } from './pages/page-not-found/page-not-found.component';
import { CustomPreloadingStrategy } from './shared/preloading-strategy/custom-preloading-strategy';

const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent
  },
  { 
    path: 'books', 
    loadChildren: () => import('./books/books.module').then(m => m.BooksModule),
    data: { preload: true }
  },
  {
    path: '',
    redirectTo: 'home',
    pathMatch: 'full'
  },
  {
    path: '**',
    component: PageNotFoundComponent
  }
];

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

Okay, I know it wasn’t really necessary to create a custom preloading strategy here. We could have used the strategy PreloadAllModules, because we only have one module to load. But this is just to show how it works.

Now we know how lazy loading works and how we can add some configuration to it. In the next post, we are going to explore route resolvers.

You can find the code of this post on my GitHub.

Lindsey is a .NET Consultant at eMenKa NV where she is focusing on web development. She is a crew-member of Techorama Belgium and the Netherlands and she runs VISUG, The Visual Studio User Group in Belgium.
Leave a Reply

Your email address will not be published. Required fields are marked *