Lazy loading with Angular 18

Updated : 14/06/2024 danny

We will implement lazy loading   in our Web Application .

For this we will use the Angular javascript framework version 18.0.2

Angular will allow us to load modules on demand.

Lazy Loading avec Angular

What are we going to do

We will configure Lazy loading in our Web Application with Angular version 18.0.2

This is step 3 of our Angular guide which will allow us to obtain a PWA type Web Application .
We will use an existing project whose characteristics are

  • Generated with Angular CLI
  • Routing

All sources created are indicated at the end of the tutorial.

The application is at the following address


Before you start

The speed at which a website is displayed is one of the most essential criteria for the user.
And this speed is measured in seconds.
Beyond 3 seconds 57% of users simply leave the site.

What methods or techniques should we use to make our website load quickly?

One of the techniques is lazy loading (“lazy or lazy loading” in French).
It has the effect of speeding up the operation of a website.
It allows you to specify which parts of a website should be loaded on startup.


Theory

Before going any further we need to understand how Angular works.
The command that interests us concerns the compilation of our project.

In our package.json file this is the command

  • ngbuild

Without going into details this command uses Webpack (a bundler module).
Thanks to Webpack Angular uses the files of our project, compiles them to generate in the dist directory a certain number of files that we can deploy on a web server.

The project which serves as our basis has 6 web pages

  • Home
  • About
  • Contact
  • ​​​​​​Login
  • Signup
  • notfound


Compiling our source code generates a main.js file which contains the code for these 6 pages (or a file like main.xxxxxxx.js )
To verify this theory, simply open the file dist/angular-starter/browser/main.js and do a search on the code used in each of the 6 pages

  • home works! (code used in home.component.html)
  • not-found-works! (code used in not-found.component.html)
  • contact works! (code used in contact.component.html)
  • login works! (code used in login.component.html)
  • signup works! (code used in signup.component.html)
  • about works! (code used in about.component.html)

This file and others will be called when displaying the website.
The greater the number of pages, the larger the file and the slower the display.

The principle of lazy loading will consist of splitting this file into several parts which will only be loaded in due time.

So let's move on to practice.


Creation of the project

Rather than recreate everything we will use a project which contains the routing.
​​​​​​​I give you the commands to run, Git is obviously mandatory to retrieve the source code.

# Créez un répertoire demo (le nom est ici arbitraire)
mkdir demo

# Allez dans ce répertoire
cd demo

# Récupérez le code source sur votre poste de travail
git clone https://github.com/ganatan/angular-react-routing

# Allez dans le répertoire qui a été créé
cd angular-react-routing
cd angular

# Exécutez l'installation des dépendances (ou librairies)
npm install

# Exécutez le programme
npm run start

# Vérifiez son fonctionnement en lançant dans votre navigateur la commande
http://localhost:4200/

Practical

Lazy loading works using the concept of modules or components (notably standalone components).

We will use the Angular documentation to apply this technique.
https://angular.io/guide/lazy-loading-ngmodules

We will adapt our architecture, by creating a module for each element to display.
Home , not-found and about will remain managed in the classic way as components.

Let's use the ng generate module command that angular-cli offers us.

# Création des modules (méthode 1)
ng generate module pages/general/contact --routing
ng generate module pages/general/login --routing
ng generate module pages/general/signup --routing

# Création des modules (méthode 2)
ng g m pages/general/contact --routing
ng g m pages/general/login --routing
ng g m pages/general/signup --routing

The files needed for each component are created automatically.

For example for the Contact component

  • contact-routing.module.ts
  • contact.module.ts


​​​​​​Lazy loading will be applied to Contact, Login and Signup
At app-route.ts we need to update the routes using loadchildren and loadcomponent.

src/app/app-routes.ts
import { Routes } from '@angular/router';

import { HomeComponent } from './pages/general/home/home.component';
import { NotFoundComponent } from './pages/general/not-found/not-found.component';

export const routes: Routes = [
  { path: '', component: HomeComponent, },

  {
    path: 'login',
    loadChildren: () => import('./pages/general/login/login.module')
      .then(mod => mod.LoginModule)
  },
  {
    path: 'signup',
    loadChildren: () => import('./pages/general/signup/signup.module')
      .then(mod => mod.SignupModule)
  },
  {
    path: 'contact',
    loadChildren: () => import('./pages/general/contact/contact.module')
      .then(mod => mod.ContactModule)
  },

  {
    path: 'about',
    loadComponent: () => import('./pages/general/about/about.component')
      .then(mod => mod.AboutComponent)
  },

  { path: '**', component: NotFoundComponent }
];

We need to modify the AppModule module and leave only the HomeComponent and NotFoundComponent components

src/app/pages/general/contact/contact.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-contact',
  templateUrl: './contact.component.html',
  styleUrl: './contact.component.css'
})
export class ContactComponent {

}
src/app/pages/general/contact/contact-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { ContactComponent } from './contact.component';

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

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ContactRoutingModule { }
src/app/pages/general/contact/contact.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { ContactComponent } from './contact.component';
import { ContactRoutingModule } from './contact-routing.module';

@NgModule({
  imports: [
    CommonModule,
    ContactRoutingModule
  ],
  exports: [
    ContactComponent
  ],
  declarations: [
    ContactComponent
  ],
  providers: [
  ],
})
export class ContactModule { }
src/app/pages/general/signup/signup.component.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { SignupComponent } from './signup.component';

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

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class SignupRoutingModule { }
src/app/pages/general/signup/signup-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { SignupComponent } from './signup.component';

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

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class SignupRoutingModule { }
src/app/pages/general/signup/signup.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { SignupComponent } from './signup.component';
import { SignupRoutingModule } from './signup-routing.module';

@NgModule({
  imports: [
    CommonModule,
    SignupRoutingModule
  ],
  exports: [
    SignupComponent
  ],
  declarations: [
    SignupComponent
  ],
  providers: [
  ],
})
export class SignupModule { }
src/app/modules/general/login/login-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { LoginComponent } from './login.component';

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

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class LoginRoutingModule { }
src/app/modules/general/login/login.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { LoginComponent } from './login.component';
import { LoginRoutingModule } from './login-routing.module';

@NgModule({
  imports: [
    CommonModule,
    LoginRoutingModule
  ],
  exports: [
    LoginComponent
  ],
  declarations: [
    LoginComponent
  ],
  providers: [
  ],
})
export class LoginModule { }

Verification

To verify the lazy loading theory we must perform a new compilation (npm run build)

In the dist/angular-starter directory this time we obtain several files in addition to the main.js file

  • main.js
  • chunk-xxx1.js
  • chunk-xxx2.js
  • chunk-xxx3.js
  • chunk-xxx4.js

Noticed:
​​​​​​​The names can be different, especially with numbers; webpack manages the naming.

The code for each of our pages is now arranged as follows:

  • home works! (code found in main.js )
  • not-found-works! (code found in main.js )
  • about works! (code used main.js )
  • contact works! (code found in chunk-xxx1.js )
  • ​​​​​​​login works! (code found in chunk-xxx2.js )
  • signup works! (code found in chunk-xxx3.js )

If we run the application (npm run start) we can see in Chrome (F12) at the Network tab how the files are loaded.

  • When the site is launched: main.js is called.
  • When selecting login: chunk-xxx2.js is called only once
  • When selecting signup: chunk-xxx3.js is called only once
  • When Contact is selected: chunk-xxx4.js is called only once


If we run the localhost/contact url

  • In this case main.js and only chunk-xxx1.js are called


Conclusion :
Regardless of the number of pages, the main.js file will always be the same size.
Launching the site that loads the main.js file will always be done at the same speed.


Child Routes

This application also contains the management of Child routes.

The different keywords that could refer to it are as follows

  • routing
  • under routing
  • subrouting
  • nested roads
  • ​​​​​​​children routes

This question is discussed in the documentation
https://angular.io/guide/router#child-route-configuration

You will find in the repository on github the addition of the notion of routing with Children.

In the routing tutorial three components were added in Contact

  • mailing
  • mapping
  • ​​​​​​​website

We need to add the notion of modules to these components.

5 files must in particular be modified to take this into account.

  • ​​​​​​contact-routing.module.ts
  • mailing-routing.module.ts
  • mapping-routing.module.ts
  • website-routing.module.ts
  • ​​​​​​​app.routes.ts

 

And 2 files need to be created.

  • ​​​​​​​about-config.ts
  • about-routes.ts
# Add mailing
ng generate module pages/general/contact/mailing --routing  --module=app

# Add mapping
ng generate module pages/general/contact/mapping --routing  --module=app

# Add website
ng generate module pages/general/contact/website --routing  --module=app
src/app/pages/general/contact/contact-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { ContactComponent } from './contact.component';

const routes: Routes = [
  {
    path: '', component: ContactComponent, children: [
      {
        path: '',
        loadChildren: () => import(`./mailing/mailing.module`)
          .then(mod => mod.MailingModule)
      },

      {
        path: 'mailing',
        loadChildren: () => import(`./mailing/mailing.module`)
          .then(mod => mod.MailingModule)
      },
      {
        path: 'mapping',
        loadChildren: () => import(`./mapping/mapping.module`)
          .then(mod => mod.MappingModule)
      },
      {
        path: 'website',
        loadChildren: () => import(`./website/website.module`)
          .then(mod => mod.WebsiteModule)
      },

      {
        path: '**',
        loadChildren: () => import(`./mailing/mailing.module`)
          .then(mod => mod.MailingModule)
      },

    ]
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class ContactRoutingModule { }
src/app/pages/general/contact/mailing/mailing-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MailingComponent } from './mailing.component';

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

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class MailingRoutingModule { }
src/app/pages/general/contact/mapping/mapping-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MappingComponent } from './mapping.component';

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

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class MappingRoutingModule { }
src/app/modules/general/contact/website/website-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { WebsiteComponent } from './website.component';

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

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class WebsiteRoutingModule { }
src/app/app-routes.ts
import { Routes } from '@angular/router';

import { HomeComponent } from './pages/general/home/home.component';
import { NotFoundComponent } from './pages/general/not-found/not-found.component';

export const routes: Routes = [
  { path: '', component: HomeComponent, },

  {
    path: 'login',
    loadChildren: () => import('./pages/general/login/login.module')
      .then(mod => mod.LoginModule)
  },
  {
    path: 'signup',
    loadChildren: () => import('./pages/general/signup/signup.module')
      .then(mod => mod.SignupModule)
  },
  {
    path: 'contact',
    loadChildren: () => import('./pages/general/contact/contact.module')
      .then(mod => mod.ContactModule)
  },

  {
    path: 'about',
    loadChildren: () => import('./pages/general/about/about.routes').then(routes => routes.routes)
  },

  { path: '**', component: NotFoundComponent }
];
about.routes.ts
import { Routes } from '@angular/router';

import { AboutComponent } from './about.component';

export const routes: Routes = [
  {
    path: '', component: AboutComponent, children: [
      {
        path: '',
        loadComponent: () => import(`./experience/experience.component`)
          .then(mod => mod.ExperienceComponent)
      },
      {
        path: 'experience',
        loadComponent: () => import(`./experience/experience.component`)
          .then(mod => mod.ExperienceComponent)
      },
      {
        path: 'skill',
        loadComponent: () => import(`./skill/skill.component`)
          .then(mod => mod.SkillComponent)
      },

      {
        path: '**',
        loadComponent: () => import(`./experience/experience.component`)
          .then(mod => mod.ExperienceComponent)
      },

    ]
  },
];
about.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './about.routes';

export const aboutConfig: ApplicationConfig = {
  providers: [provideRouter(routes)]
};

Tests

Before carrying out we must adapt the corresponding test files.

  • ​​​​​​​signup.component.spec.ts
  • contact.component.spec.ts
  • ​​​​​​​login.component.spec.ts


All that will then remain is to test the application.

signup.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { SignupComponent } from './signup.component';

describe('SignupComponent', () => {
  let component: SignupComponent;
  let fixture: ComponentFixture<SignupComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [SignupComponent]
    });
    fixture = TestBed.createComponent(SignupComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
contact.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { ContactComponent } from './contact.component';

describe('ContactComponent', () => {
  let component: ContactComponent;
  let fixture: ComponentFixture<ContactComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],      
      declarations: [ContactComponent]
    });
    fixture = TestBed.createComponent(ContactComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
login.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { LoginComponent } from './login.component';

describe('LoginComponent', () => {
  let component: LoginComponent;
  let fixture: ComponentFixture<LoginComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [LoginComponent]
    });
    fixture = TestBed.createComponent(LoginComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
# Développement
npm run start
http://localhost:4200/

# Tests
npm run lint
npm run test

# Production
npm run build

Source code

The source code used at the start of the tutorial is available on github
https://github.com/ganatan/angular-react-routing

The source code obtained at the end of this tutorial is available on github
https://github.com/ganatan/angular-react-lazy-loading

The following steps will allow you to obtain a prototype application.

 

​​​​​​​​​​​​​​
This last step allows you to obtain an example application


The source code for this final application is available on GitHub
https://github.com/ganatan/angular-app