Lazy loading avec Angular 18

Mis à jour : 14/06/2024 danny

Nous allons implémenter le lazy loading dans notre Application Web.

Nous utiliserons pour cela le framework javascript Angular version 18.0.2

Angular nous permettra de charger les modules à la demande.

Lazy Loading avec Angular
Lazy Loading avec Angular

Si vous n'avez pas le temps de lire ce guide en entier,
téléchargez le maintenant


Qu'allons nous faire

Nous allons configurer le Lazy loading dans notre Application Web avec  Angular version 18.0.2​​​

Il s'agit de l'étape 3 de notre guide Angular qui nous permettra d'obtenir une Application Web de type PWA.
Nous allons utiliser un projet existant dont les caractéristiques sont

  • Genéré avec Angular CLI
  • Routing

Tous les sources créés sont indiqués en fin de tutoriel.

L' application est à l'adresse suivante 


Avant de commencer

La vitesse à laquelle s'affiche un site web est l'un des critères les plus essentiels pour l'utilisateur.
Et cette vitesse s'apprécie en secondes.
Au-delà de 3 secondes 57% des utilisateurs quittent purement et simplement le site.

Quelles méthodes ou techniques doit-on alors utiliser pour que notre site web se charge rapidement ?

L'une des techniques est le lazy loading (“chargement fainéant ou paresseux” en français).
Il a pour effet d'accélérer le fonctionnement d'un site web.
Il permet de spécifier quelles parties d'un site web doivent être chargées lors du démarrage.
 


Théorie

Avant d'aller plus loin il nous faut comprendre comment fonctionne Angular.
La commande qui nous intéresse concerne la compilation de notre projet.

Dans notre fichier package.json il s'agit de la commande

  • ng build

Sans rentrer dans les détails cette commande utilise Webpack (un module bundler).
Grâce à Webpack angular utilise les fichiers de notre projet , les compile pour générer dans le répertoire dist un certain nombre de fichiers que nous pourrons déployer sur un serveur web.

Le projet qui nous sert de base dipose de 6 pages Web

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


La compilation de notre code source génère notamment un fichier main.js qui contient le code de ces 6 pages (ou un fichier du type main.xxxxxxx.js)
Pour vérifier cette théorie il suffit d'ouvrir le fichier dist/angular-starter/browser/main.js faire une recherche sur le code utilisé dans chacune des 6 pages

  • home works! (code utilisé dans home.component.html)
  • not-found works! (code utilisé dans not-found.component.html)
  • contact works! (code utilisé dans contact.component.html)
  • login works! (code utilisé dans login.component.html)
  • signup works! (code utilisé dans signup.component.html)
  • about works! (code utilisé dans about.component.html)

Ce fichier et d'autres seront appelés lors de l'affichage du site Web.
Plus le nombre de pages sera grand, plus le fichier sera volumineux et plus l'affichage sera lent.

Le principe du lazy loading va consister à scinder ce fichier en plusieurs parties qui ne seront chargées qu'en temps voulu.

Passons donc à la pratique.


Création du projet

Plutôt que de tout recréer nous allons utiliser un projet qui contient le routing.
​​​​​​​Je vous donne les commandes à lancer , Git est évidemment obligatoire pour récupérer le code source.

# 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/

Pratique

Le lazy loading fonctionne en utilisant la notion de modules ou celle de composant (notamment les composants standalone).

Nous utiliserons la documentation Angular pour appliquer cette technique.
https://angular.io/guide/lazy-loading-ngmodules

Nous allons adapter notre architecture, en créant un module pour chaque élément à afficher.
Home , not-found et about resteront gérés de façon classique sous forme de composants.

Utilisons la commande ng generate module que nous offre angular-cli.

# 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

Les fichiers nécessaires à chaque composant sont créés automatiquement.

Par exemple pour le composant Contact

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

 

Le lazy loading sera appliqué sur Contact, Login et Signup
Au niveau de app-route.ts, nous devons mettre à jour les routes en utilisant loadchildren et 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 }
];

Il ne reste à modifier les fichiers de routing et module pour Contact, Login, et Signup.

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 { Component } from '@angular/core';

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

}
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/pages/general/login/login.component.ts
import { Component } from '@angular/core';

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

}
src/app/pages/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/pages/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 { }

Vérification

Pour vérifier la théorie du lazy loading nous devons effectuer une nouvelle compilation (npm run build)

Dans le répertoire dist/angular-starter nous obtenons cette fois plusieurs fichiers en plus du fichier main.js

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

Remarque:
​​​​​​​Les noms peuvent être différents notamment avec des numéros c'est webpack qui gère le nommage.

Le code de chacune de nos pages est maintenant disposé de la façon suivante

  • home works! (code trouvé dans main.js)
  • not-found works! (code trouvé dans main.js)
  • about works! (code utilisé main.js)
  • contact works! (code trouvé dans chunk-xxx1.js)
  • ​​​​​​​login works! (code trouvé dans chunk-xxx2.js)
  • signup works! (code trouvé dans chunk-xxx3.js)

Si nous exécutons l'application (npm run start) nous pouvons voir dans Chrome (F12) au niveau de l'onglet Network comment les fichiers sont chargés.

  • Au lancement du site : main.js est appelé.
  • A la sélection de login: chunk-xxx2.js est appelé une seule fois
  • A la sélection de signup: chunk-xxx3.js est appelé une seule fois
  • A la sélection de Contact : chunk-xxx4.js est appelé une seule fois


Si nous lançons l'url localhost/contact

  • Dans ce cas main.js et seulement chunk-xxx1.js sont appelés


Conclusion : 
Quelle que soit le nombre de pages, le fichier main.js aura toujours la même taille.
Le lancement du site qui charge le fichier main.js se fera toujours à la même vitesse. 


Child Routes

Cette application contient aussi la gestion des Child routes.

Les différents mots clé qui pourraient y faire référence sont les suivants

  • routing
  • sous routing
  • sub routing
  • nested routes
  • ​​​​​​​children routes​​​​​​​

Cette question est évoquée dans la documentation
https://angular.io/guide/router#child-route-configuration

Vous retrouverez dans le dépôt sur github l'ajout de la notion de routing avec Children.

Dans le tutoriel sur le routing trois composants ont été rajoutés dans Contact

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

Il nous faut rajouter la notion de modules sur ces composants.

5 fichiers doivent notamment être modifiés pour en prendre compte.

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

 

Et 2 fichiers doivent être créés.

  • ​​​​​​​about-config.ts
  • about-routes.ts​​​​​​​​​​​​​​
# Rajout du module mailing
ng generate module pages/general/contact/mailing --routing

# Rajout du module mapping
ng generate module pages/general/contact/mapping --routing

# Rajout du module website
ng generate module pages/general/contact/website --routing
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

Avant d'effectuer il nous faut adapter les fichiers de tests correspondant.

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


Il ne restera alors plus qu' à tester l'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

Code source

Le code source utilisé en début de tutoriel est disponible sur github
https://github.com/ganatan/angular-react-routing

Le code source obtenu à la fin de ce tutoriel est disponible sur github
https://github.com/ganatan/angular-react-lazy-loading

Les étapes suivantes vous permettront d'obtenir une application prototype.

 

Les étapes suivantes vous permettront d'améliorer ce prototype​​​​​​​

​​​​​​​
Cette dernière étape permet d'obtenir un exemple d'application


Le code source de cette application finale est disponible sur GitHub
https://github.com/ganatan/angular-app

 

Comment créer une application From scratch ?

Créez votre compte ganatan

Téléchargez gratuitement vos guides complets

Démarrez avec angular CLI Démarrez avec angular CLI

Gérez le routing Gérez le routing

Appliquez le Lazy loading Appliquez le Lazy loading

Intégrez Bootstrap Intégrez Bootstrap


Utilisez Python avec Angular Utilisez Python avec Angular

Utilisez Django avec Angular Utilisez Django avec Angular

Utilisez Flask avec Angular Utilisez Flask avec Angular

Ganatan site Web avec Angular