🇺🇸 EN
Angular
·6 min read·↗·🇺🇸 EN

Cómo optimizar el rendimiento con Lazy Loading y precarga de rutas en Angular

Portada principal del artículo: Cómo optimizar el rendimiento con Lazy Loading y precarga de rutas en Angular

⚡ ¿Estás usando Angular moderno (v17+)? Ve directo a Lazy Loading moderno con Standalone Components y @defer para ver loadComponent, provideRouter() y las vistas con @defer en acción.

Cuando construimos una aplicación empresarial en Angular con múltiples funcionalidades, empaquetar todos los componentes en un único archivo main.js afecta directamente el tiempo de carga inicial. Los usuarios terminan esperando por pantallas y funciones que tal vez ni siquiera lleguen a visitar durante su sesión.

Para lograr métricas excelentes en Core Web Vitals y ofrecer una experiencia rápida, necesitamos dividir la aplicación en fragmentos bajo demanda usando Lazy Loading y estrategias de precarga.

En esta guía, te voy a mostrar cómo funciona el lazy loading tanto en arquitecturas heredadas con NgModule como en aplicaciones modernas con Standalone, cómo configurar estrategias de precarga personalizadas y cómo combinar la división de rutas con vistas @defer a nivel de plantilla.


El escenario: una aplicación de pagos con múltiples módulos

Imagina que estamos construyendo un panel de control para una fintech con cuatro secciones bien definidas:

  1. Dashboard (pantalla principal)
  2. MoneyTransfer
  3. Wallet
  4. Activity

Veamos qué pasa cuando empaquetamos todas estas funciones juntas frente a cargarlas de forma diferida con lazy loading.


1. Enfoque heredado: Lazy Loading y precarga con NgModule

En versiones anteriores de Angular (de v2 a v16), el lazy loading requería un módulo de rutas Routing.module dedicado para cada funcionalidad:

// app-routing.module.ts (NgModule tradicional)
import { NgModule } from '@angular/core';
import { RouterModule, Routes, PreloadAllModules } from '@angular/router';
 
const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  {
    path: 'wallet',
    loadChildren: () => import('./wallet/wallet.module').then((m) => m.WalletModule),
  },
  {
    path: 'activity',
    loadChildren: () => import('./activity/activity.module').then((m) => m.ActivityModule),
  },
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module').then((m) => m.DashboardModule),
  },
  {
    path: 'money',
    loadChildren: () => import('./moneytransfer/moneytransfer.module').then((m) => m.MoneyTransferModule),
  },
];
 
@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      preloadingStrategy: PreloadAllModules, // Precarga todos los módulos en segundo plano
    }),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Aunque esto reducía el tamaño inicial del archivo main.js, tener que crear módulos de rutas intermedios para cada pantalla generaba demasiado código repetitivo innecesario.

Afortunadamente, las versiones recientes de Angular eliminaron toda esta fricción. Veamos cómo manejar la carga diferida sin módulos.


Lazy Loading moderno con Standalone Components y @defer

En el Angular moderno (v17+ y v19), configuramos el enrutamiento de forma limpia sin NgModule, usando provideRouter() y loadComponent:

1. Lazy Loading a nivel de ruta con loadComponent

// app.routes.ts (Rutas Standalone modernas)
import { Routes } from '@angular/router';
 
export const routes: Routes = [
  { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
  {
    path: 'dashboard',
    loadComponent: () =>
      import('./dashboard/dashboard.component').then((m) => m.DashboardComponent),
  },
  {
    path: 'wallet',
    loadComponent: () =>
      import('./wallet/wallet.component').then((m) => m.WalletComponent),
    data: { preload: true }, // Indicador en la ruta para precarga personalizada
  },
  {
    path: 'money',
    loadComponent: () =>
      import('./money/money.component').then((m) => m.MoneyComponent),
  },
  {
    path: 'activity',
    loadChildren: () =>
      import('./activity/activity.routes').then((m) => m.ACTIVITY_ROUTES),
  },
];

2. Configuración de la aplicación con provideRouter

En app.config.ts, inicializamos el router con las funciones modernas:

// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
import { routes } from './app.routes';
 
export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(
      routes,
      withPreloading(PreloadAllModules) // Helper nativo para precarga
    ),
  ],
};

Con withPreloading(PreloadAllModules), Angular descarga todos los fragmentos diferidos en segundo plano una vez que la ruta inicial termina de renderizarse. Sin embargo, precargar absolutamente todo no siempre es la mejor idea en aplicaciones grandes.


Cómo crear una estrategia de precarga personalizada

PreloadAllModules descarga todos los fragmentos diferidos inmediatamente después de que la página inicial se renderiza. En aplicaciones grandes, esto puede consumir datos móviles de los usuarios de forma innecesaria.

Una estrategia de precarga personalizada te permite precargar de manera selectiva solo las rutas de alta prioridad (por ejemplo, aquellas que marcamos con data: { preload: true }):

// custom-preloading.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
 
@Injectable({
  providedIn: 'root',
})
export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Comprobamos si la ruta tiene activa la propiedad de precarga
    if (route.data && route.data['preload']) {
      return load();
    }
    return of(null);
  }
}

Registro de la estrategia en app.config.ts

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withPreloading } from '@angular/router';
import { routes } from './app.routes';
import { SelectivePreloadingStrategy } from './custom-preloading.strategy';
 
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(SelectivePreloadingStrategy)
    ),
  ],
};

Dividir el código a nivel de rutas es excelente para separar páginas enteras, pero ¿qué pasa cuando tenemos componentes pesados dentro de una misma vista? Ahí es donde entra @defer.


Lazy Loading granular con bloques @defer

El lazy loading a nivel de ruta divide páginas completas. Pero ¿qué ocurre con los widgets pesados dentro de una pantalla, como gráficos de transacciones o herramientas de exportación a PDF?

Angular moderno introduce las vistas diferibles (@defer) para cargar componentes individuales directamente desde las plantillas:

<!-- dashboard.component.html -->
<div class="dashboard-container">
  <h1>Welcome to your Dashboard</h1>
 
  <!-- Contenido crítico inmediato -->
  <app-account-summary />
 
  <!-- Carga diferida fuera de pantalla cuando el usuario hace scroll hacia ella -->
  @defer (on viewport) {
    <app-heavy-spending-chart [transactions]="transactions()" />
  } @placeholder {
    <div class="chart-skeleton">Cargando análisis de gastos...</div>
  } @loading (minimum 300ms) {
    <div class="spinner">Obteniendo recursos del gráfico...</div>
  } @error {
    <p>Error al cargar el gráfico de análisis.</p>
  }
</div>

Ventajas de usar @defer para dividir código a nivel de componentes:

  1. Cero configuración en el router: El compilador de Angular genera de forma automática un chunk de JavaScript independiente para <app-heavy-spending-chart>.
  2. Disparadores interactivos: Puedes cargar por viewport, interaction (clic o foco), hover, timer(2s) o cuando el navegador esté en idle.
  3. Estados de experiencia de usuario integrados: Los bloques nativos @placeholder, @loading y @error evitan saltos de diseño y mejoran la percepción visual.

Para decidir cuándo conviene usar cada una de estas herramientas, veamos una tabla comparativa de sus casos de uso.


Matriz comparativa de estrategias

EstrategiaCuándo usarlaBeneficio
loadComponentSeparar vistas y páginas completas de la aplicaciónReduce el tamaño del bundle inicial main.js
PreloadAllModulesAplicaciones pequeñas o medianas con conexiones establesTransiciones instantáneas entre páginas tras la carga inicial
SelectivePreloadingStrategyAplicaciones grandes donde cuidamos el consumo de datos móvilesPrecarga únicamente las rutas más frecuentadas o críticas
@defer (on viewport)Widgets pesados, gráficas, modales y editores enriquecidosPospone la descarga de JavaScript hasta que el componente es visible

Resumen

Optimizar el rendimiento en Angular moderno requiere un enfoque por capas:

  1. Usa loadComponent en tus definiciones de rutas para dividir páginas en fragmentos Standalone.
  2. Configura withPreloading(SelectivePreloadingStrategy) en provideRouter() para precargar rutas clave en segundo plano.
  3. Apóyate en @defer (on viewport) dentro de las plantillas para posponer componentes o widgets de terceros pesados hasta que el usuario realmente los necesite.
Parte de la serie Angular

These are my experiences learning and facing my daily challenges working with Angular.

Ver serie completa

Preguntas Frecuentes

¿Cuál es la diferencia entre loadChildren y loadComponent en el enrutamiento de Angular?

loadChildren se usa para cargar configuraciones de rutas hijas o NgModules heredados de forma diferida, mientras que loadComponent permite cargar directamente un componente Standalone sin necesidad de ningún módulo envoltorio.

¿Cómo se configuran las estrategias de precarga en aplicaciones Angular modernas con Standalone?

Usa provideRouter(routes, withPreloading(PreloadAllModules)) dentro de app.config.ts en lugar de pasar opciones a RouterModule.forRoot().

¿En qué se diferencia el bloque @defer del lazy loading a nivel de rutas en Angular?

El lazy loading de rutas divide páginas completas en bundles de JavaScript separados que se descargan al cambiar la URL, mientras que @defer permite una carga diferida granular a nivel de plantilla para componentes o widgets específicos según las interacciones del usuario (como scroll en el viewport, hover o estado inactivo).

¿Cómo crear una estrategia de precarga de rutas personalizada en Angular?

Implementa la interfaz PreloadingStrategy, evalúa las propiedades de data en la ruta (por ejemplo, if (route.data?.preload) return load()), y registra la clase en provideRouter(routes, withPreloading(MyCustomStrategy)).

Artículos Relacionados

Compartir este artículo

Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.


Software real. Lecciones reales.

Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.

User avatar
User avatar
User avatar
User avatar
+13K

Únete a 13,800+ desarrolladores y lectores.

Cero spam. Cancela tu suscripción en cualquier momento.