Lazy loading en Angular

Publicado el 13.01.2021 a las 05:42

Lazy loading en Angular

Lazy loading en Angular

Hoy en día, en todo proyecto de Angular que se inicie de cierta envergadura, se debería de utilizar el patrón lazy load para mejorar la experencia de usuario aumentado la velocidad de carga de la aplicación

¿En qué consiste el patrón lazy loading?

El lazy loading (o carga asíncrona o diferida o perezosa) es un patrón de diseño que se utiliza para aumentar la velocidad de carga de una aplicación y consiste en retrasar la inicialización de algunos componentes u objetos hasta el momento de su utilización. Este proceso mejora el rendimiento de las aplicaciones, puesto que al iniciar la aplicación sólo se inicializará los componentes del módulo principal (app.module.ts), y el resto de componentes se ubican en módulos diferentes que irán cargando a medida que los vayamos necesitando.


Por ejemplo, imagina que tenemos una aplicación para vender bicicletas, cuando cargamos la aplicación, sólo se cargará el componente para mostrar las bicicletas en ventas, pero los componentes referidos a la autenticación (login, register, forgot_password...) se cargarán en un módulo por ejemplo llamado auth y que no se cargarán hasta que no llamemos a alguno de los componentes que hay dentro (login, reister, forgot_password...). De la misma forma podríamos tener un módulo para las ventas donde tendríamos los componentes de la cesta, del listado de pedidos de un usuario...


Para usar lazy load en Angular, hacemos llamado de un módulo mediante el sistema de rutas de Angular y este módulo a su vez tiene rutas hijas que se encargan de cargar el componente solicitado por el usuario.

Ejemplo práctico

  1. Creamos un nuevo proyecto en Angular

    ng new LazyLoad

    Contestamos que sí a las preguntas de si queremos ayuda para depurar bugs y en si deseamos usar el router de Angular.

    Elegimos la opción de usar estilos CSS. No será objeto de este ejemplo hacer una aplicación con muchos estilos.

  2. Creamos los módulos

    Creamos los diferentes módulo que va a tener el proyecto, en nuestro caso van a ser 3, home, about y contact

    ng g m modules/home --routing
    ng g m modules/about --routing
    ng g m modules/contact --routing

    la g significa genera, la m significa módulo y el flag del final --routing lo que le indica a Angular es que genere un fichero para la rutas del módulo que crea.


    El resultado del punto anterior será que en la carpeta src/app se ha creado una carpeta llamada modules, y dentro de esa carpeta hay 3 carpetas más, home, about y contact. Y dentro de cada una de esas carpetas encontraremos 2 ficheros de typescript, uno será el módulo (home.module.ts) y el otro será el fichero de rutas (home-routing.module.ts)

  3. Creamos los componentes a cada módulo

    Vamos a crear 2 componentes (componente01 y componente02) por cada módulo

    ng g c modules/home/componente01 --skipTests
    ng g c modules/home/componente02 --skipTests
    ng g c modules/about/componente01 --skipTests
    ng g c modules/about/componente02 --skipTests
    ng g c modules/contact/componente01 --skipTests
    ng g c modules/contact/componente02 --skipTests

    El flag --skipTests es para decirle a Angular CLI que no me genere el fichero de pruebas o tests unitarios.

    Con las instrucciones anteriores, tendremos en cada uno de los módulos 2 nuevas carpetas, una carpeta para el componente01 y otra para el componente02.

  4. Componente de página 404

    La página 404 hace referencia al código de respuesta 404, puedes ver más acerca de los códigos de estado en este artículo

    Este componente lo creo por si el usuario introuce una ruta que no tengo recogido en la barra de dirección del navegador

    ng g c shared/Page404 --skip-tests

    Angular CLI, automáticamente lo importará al app.module.ts

  5. Creamos las rutas hijas

    En el fichero de routing de cada uno de los módulos, añadiremos las rutas hijas para carga del componente01 y del componente02 de cada uno de los módulos.

    Los fichero de routing tienen que quedar:

      import { NgModule } from '@angular/core';
      import { Routes, RouterModule } from '@angular/router';
    
      import { Componente02Component } from './componente02/componente02.component';
      import { Componente01Component } from './componente01/componente01.component';
    
      const routes: Routes = [
        {
          path: "",
          children: [
            { path: "uno", component: Componente01Component },
            { path: "dos", component: Componente02Component },      
            { path: "**", redirectTo: "uno"}      
          ],
        },
      ];
    
      @NgModule({
        imports: [RouterModule.forChild(routes)],
        exports: [RouterModule]
      })
      export class HomeRoutingModule { }
    
  6. Configurando el fichero principal de rutas

    Es hora de programar el fichero principal de rutas app-routing.module.ts

      import { NgModule } from '@angular/core';
      import { Routes, RouterModule } from '@angular/router';
      import { Page404Component } from './shared/page404/page404.component';
    
      const routes: Routes = [
        { 
          path: '', redirectTo: 'home', pathMatch: 'full'
        },
        {
          path: 'home',
          loadChildren: () => import('../app/modules/home/home.module').then(m => m.HomeModule)      
        },
        {
          path: 'contact',
          loadChildren: () => import('../app/modules/contact/contact.module').then(m => m.ContactModule)      
        },
        {
          path: 'about',
          loadChildren: () => import('../app/modules/about/about.module').then(m => m.AboutModule)      
        },  
        {
          path: '**', component: Page404Component
        },
      ];
    
      @NgModule({
        imports: [RouterModule.forRoot(routes)],
        exports: [RouterModule]
      })
      export class AppRoutingModule { }
      
  7. Atención: es importante que en los imports del app.module.ts no estén los diferentes módulos que queremos que se carguen en lazy load, ya que si sí están, se cargarán todos los módulos al iniciar la aplicación. A mí me paso en una ocasión y me llevó como 2 horas encontrar el problema.

  8. Creación de los elementos visuales HTML

    Borrar todo el contenido del app.component.html y cambiar por:

      <div>
        <h1>
          Lazy Loading
        </h1>
        <ul>
          <li>
            <a routerLink="/home">Home</a>
            <ul>
              <li><a routerLink="/home/uno">Componente 1</a></li>
              <li><a routerLink="/home/dos">Componente 2</a></li>
            </ul>
          </li>    
          <li>
            <a routerLink="/about">About</a>
            <ul>
              <li><a routerLink="/about/uno">Componente 1</a></li>
              <li><a routerLink="/about/dos">Componente 2</a></li>
            </ul>
          </li>
          <li>
            <a routerLink="/contact">Contact</a>
            <ul>
              <li><a routerLink="/contact/uno">Componente 1</a></li>
              <li><a routerLink="/contact/dos">Componente 2</a></li>
            </ul>
          </li>
        </ul>
      </div>
      <router-outlet></router-outlet>
      

    Con lo anterior creamos una página HTML sin estilos para poder navegar entre los diferente módulos y entre sus diferentes componentes

  9. Comprobación de la carga asíncrona, diferida, perezosa o lazy load

    Para comprobar la carga asíncrona

    1. Abre la aplicación
    2. Abre la consola del navegador
    3. Ve a la pestaña red o network
    4. Recarga la aplicación
    5. Si ordenas los recursos por nombre, el primero es app-modules-home-home-module.js que es el que se carga por defecto porque así lo hemos programado en la primera ruta del app-routing.module.ts
    6. Si haces clic en cualquiera de los dos componentes de about, verás que se carga el módulo de about con el nombre de app-modules-about-about-module.js. Esta es la magia del lazy loading

Precaución si la ruta está protegido por un guard

En el caso de que tengamos un módulo protegido por un guard, hay programar el método canLoad() en el guard.


Imagina que en el ejmplo anterior, quieres proteger el acceso al módulo about con un guard llamando AuthGuard porque quieres que el usuario esté autenticado para poder acceder al módulo.

El archivo de rutas quedaría:

  import { NgModule } from '@angular/core';
  import { Routes, RouterModule } from '@angular/router';
  import { Page404Component } from './shared/page404/page404.component';

  const routes: Routes = [
    { 
      path: '', redirectTo: 'home', pathMatch: 'full'
    },
    {
      path: 'home',
      loadChildren: () => import('../app/modules/home/home.module').then(m => m.HomeModule)      
    },
    {
      path: 'contact',
      loadChildren: () => import('../app/modules/contact/contact.module').then(m => m.ContactModule)      
    },
    {
      path: 'about',
      canActivate: [ AuthGuard ],
      canLoad:[ AuthGuard ],
      loadChildren: () => import('../app/modules/about/about.module').then(m => m.AboutModule)      
    },  
    {
      path: '**', component: Page404Component
    },
  ];

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

Y el guard(auth.guard.ts) sería algo como por ejemplo:

  import { tap } from 'rxjs/operators';
  import { tap } from 'rxjs/operators';
  import { UsuarioService } from './../services/usuario.service';
  import { Injectable } from '@angular/core';
  import {
    CanActivate,
    ActivatedRouteSnapshot,
    RouterStateSnapshot,
    Router,
    CanLoad,
    Route,
    UrlSegment,
    UrlTree,
  } from '@angular/router';
  import { Observable } from 'rxjs';
  
  @Injectable({
    providedIn: 'root',
  })
  export class AuthGuard implements CanActivate, CanLoad {
    constructor(private usuarioService: UsuarioService, private router: Router) {}
    canLoad(
      route: Route,
      segments: UrlSegment[]
    ):
      | boolean
      | UrlTree
      | Observable<boolean | UrlTree>
      | Promise<boolean | UrlTree> {
      return this.usuarioService.validarToken().pipe(
        tap((estaAutenticado) => {
          if (!estaAutenticado) {
            this.router.navigateByUrl('/home');
          }
        })
      );
    }
  
    canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
      return this.usuarioService.validarToken().pipe(
        tap((estaAutenticado) => {
          if (!estaAutenticado) {
            this.router.navigateByUrl('/home');
          }
        })
      );
    }
  }  

Recursos en línea

Si lo deseas puedes clonar el repositorio del ejemplo de mi GitHub

Cualquier duda o comentario puedes enviarla por aquí


Hasta luego 🖖

fjmduran.com v 17.0.1