Lazy loading

<< Route guards Formularios >>

Carga en diferido de rutas: Lazy loading

Para mejorar el rendimiento en la carga inicial de la aplicación, podríamos agrupar rutas que tengan un prefijo común (en nuestro caso /products) y cargarlas en diferido. Es decir, el script inicial que se cargaría en el navegador no incluiría el código asociado a los componentes de esas rutas, ni guards, resolvers, etc. Esto implicaría un tiempo de carga menor y una mejor experiencia de usuario.

Para tener una página inicial que no tenga que ver con las rutas de productos y dejar esa fuera de la división de código, vamos a crear un componente que represente una página inicial de la aplicación (y su correspondiente ruta). Lo llamaremos welcome.

ng g c welcome

El contenido del componente es lo de menos, simplemente pondremos un mensaje de bienvenida en la plantilla.

<div class="p-5 mb-4 bg-light rounded-3">
  <div class="container-fluid py-5">
    <h1 class="display-5 fw-bold">Bienvenido/a</h1>
    <p class="col-md-8 fs-4">En esta aplicación descubriremos los secretos de Angular.</p>
    <a class="btn btn-primary btn-lg" [routerLink]="['/products']">Ver listado de productos</a>
  </div>
</div>

Seguidamente, asignaremos la ruta por defecto a este componente

export const routes: Routes = [
  {
    path: 'welcome',
    component: WelcomeComponent,
    title: 'Bienvenido | Angular Products'
  },
  //...
  { path: '', redirectTo: '/welcome', pathMatch: 'full' },
  { path: '**', redirectTo: '/welcome' },
];

Después, creamos un directorio llamado products dentro de src/app. Y metemos ahí todo lo estrictamente relacionado con las rutas de productos (cuyo prefijo es /products). Esto no es obligatorio, pero ayuda a organizar el código. Lo que pensemos que podríamos reutilizar para otras rutas en el futuro (por ejemplo los guards que hemos creado, o el componente star-rating), o sea global (interceptores), se quedará en src/app, o lo podríamos meter en un directorio llamado shared por ejemplo.

Posteriormente, creamos un archivo llamado products.routes.ts dentro de la carpeta products. Este archivo contendrá las rutas relacionadas con productos. Copiamos todas las rutas que empiezan por products aquí (Importante: omitiendo el prefijo products/).

export const productsRoutes: Routes = [
  {
    path: '',
    component: ProductsPageComponent,
    title: 'Productos | Angular Products',
  },
  {
    path: 'add',
    canDeactivate: [leavePageGuard],
    component: ProductFormComponent,
    title: 'Añadir producto | Angular Products',
  },
  {
    path: ':id',
    canActivate: [numericIdGuard],
    resolve: {
      product: productResolver,
    },
    component: ProductDetailComponent,
  },
];

Posteriormente, una vez quitadas esas rutas del archivo principal (app.routes.ts), añadimos una ruta que representará el prefijo 'products' y con la propiedad loadChildren importamos de forma dinámica las rutas que hemos separado en el otro archivo.

export const routes: Routes = [
  {
    path: 'welcome',
    component: WelcomeComponent,
    title: 'Bienvenido | Angular Products'
  },
  {
    path: 'products',
    loadChildren: () => import('./products/products.routes').then(m => m.productsRoutes)
  },
  { path: '', redirectTo: '/welcome', pathMatch: 'full' },
  { path: '**', redirectTo: '/welcome' },
];

Estas rutas se cargarán en diferido (lazy loading). Esto quiere decir, que todo el código que tenga que ver con las rutas de productos se exportará a un archivo JavaScript separado, y este solo se cargará la primera vez que visitemos una ruta con ese prefijo.

Vamos a ver como quedaría el tamaño del código generado (para producción, con ng build) como lo teníamos antes y con la carga en diferido de las rutas.

Antes:

Después:

Antes el navegador tenía que cargar unos 295KB de código (78KB comprimidos) para lanzar la aplicación. Este código incluía todo, nuestro código, librerías y Angular. Ahora ha pasado lo siguiente:

  • El código común (librerías, Angular) se ha llevado a un archivo aparte de unos 254KB (que se tiene que cargar al inicio).
  • Nuestro código inicial que se carga en el inicio de la app (componente welcome y app, y poco más), ocupa solo 2,27 KB
  • Todo lo que tenga que ver con las rutas de productos, ocupa unos 40KB y no se carga al inicio (lazy loading).

Si sumamos todo lo anterior, nos da más o menos, el mismo tamaño de código que generaba antes. Un buen porcentaje de ese código es Angular y otras librerías, por lo que no parece que estemos ahorrando mucho, pero en una aplicación de verdad, nuestro código va a representar un porcentaje mucho mayor del total que en este ejemplo. Por lo que separar gran parte del mismo para que no se cargue al principio, se va a notar mucho en los tiempos de carga iniciales.

Carga en diferido de componentes en las rutas

Se puede optar a un nivel más fino de optimización si además, cargamos en diferido cada componente asociado a una ruta usando loadComponent en lugar de component. De esta manera, cada ruta individual, tendría su código separado del resto, y al visitar una página por primera vez solo se cargaría el código asociado a dicha página. Una vez se visita una ruta, durante la ejecución del programa, ya estará cargado para las siguientes visitas a la misma.

Seguiremos manteniendo en app.routes.ts la carga de product.routes.ts en diferido. Ya que aunque el código de cada componente tendrá su propio archivo que solo se cargará al visitar la ruta asociada al mismo, es interesante que los guards, resolvers, etc asociados a las rutas de productos no se incluyan en el código principal de la aplicación.

export const routes: Routes = [
  {
    path: 'welcome',
    loadComponent: () =>
      import('./welcome/welcome.component').then((m) => m.WelcomeComponent),
    title: 'Bienvenido | Angular Products'
  },
  {
    path: 'products',
    loadChildren: () => import('./products/products.routes').then(m => m.productsRoutes)
  },
  { path: '', redirectTo: '/welcome', pathMatch: 'full' },
  { path: '**', redirectTo: '/welcome' },
];

Estrategia de precarga

El comportamiento predeterminado de carga en diferido consiste en cargar las rutas o componentes solo cuando accedemos a una ruta que los necesita. Existe otra estrategia llamada precarga. Esta cargará primero la aplicación principal sin los componentes de carga diferida (tiempo de carga rápido), pero tan pronto como termine de cargar la aplicación, descargará e importará el resto de los componentes en segundo plano. Así, cuando vayamos a la ruta 'products', por ejemplo, el componente ProductsPageComponent ya estará cargado en memoria.

Para usar esta estrategia, en el archivo app.config.ts, incluya la función withPreloading con la opción PreloadAllModules.

export const appConfig: ApplicationConfig = {
  providers: [
    //...
    provideRouter(routes, withComponentInputBinding(), withPreloading(PreloadAllModules)),
    //...
  ],
};

<< Route guards Formularios >>