Volver al Blog
Angular Development15 de julio de 20268 min de lectura

Angular Lazy Loading: Apps más rápidas con menos esfuerzo

DR

Doug Ribeiro

8 min de lectura

Lo primero que hago en cualquier codebase Angular es ejecutar un análisis de bundle. El patrón que busco es familiar: un usuario que abre la primera pantalla descarga código de features y pasos del flujo a los que quizás nunca llegue. Lazy loading es la solución, y puede entrar de la forma más aburrida: una ruta de feature a la vez, mediante pull requests revisados normales. Sin heroicidades, sin refactor big-bang, y aun así la mejora de rendimiento más barata que conozco. Los ejemplos de código de abajo usan un dominio de solicitud de préstamos solo como ilustración; ninguno viene del codebase de un empleador.

Por qué la configuración por defecto de Angular te perjudica

Coincidencia de ruta loadComponent() Petición de red Inicialización del componente Renderizado

El build por defecto de Angular pone todo en un solo chunk principal. Cada componente que se importa en cualquier parte de tu árbol de módulos cargados eagerly termina en ese chunk. Un módulo de reportes importado en app.module.ts o app.config.ts arrastra cada librería de gráficos, cada componente de tabla, y cada servicio que reportes necesita. El usuario que solicita la pantalla de login paga por todo.

El lazy loading mueve el código específico de cada ruta a chunks separados que se obtienen bajo demanda. Un usuario que nunca navega a reportes nunca descarga el código de reportes. El First Contentful Paint mejora. El Time to Interactive mejora. Los puntajes de Lighthouse mejoran. Y la experiencia real del usuario mejora en las conexiones que importan, que nunca son las conexiones de fibra en las que los developers testean.

loadComponent para componentes de ruta única

Con standalone components, son tres líneas de código por ruta.

typescript
// app.routes.ts
export const routes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./features/dashboard/dashboard.component')
        .then(m => m.DashboardComponent)
  },
  {
    path: 'applications',
    canActivate: [authGuard],
    loadComponent: () =>
      import('./features/applications/applications.component')
        .then(m => m.ApplicationsComponent)
  },
  {
    path: 'reports',
    canActivate: [authGuard, roleGuard('analyst')],
    loadComponent: () =>
      import('./features/reports/reports.component')
        .then(m => m.ReportsComponent)
  }
];

El import dinámico es la frontera. El compilador de Angular crea un chunk separado para todo lo alcanzable desde ese import. Si ReportsComponent importa una librería de gráficos de 400KB y un renderizador de PDF de 200KB, esos se quedan fuera del bundle principal.

loadChildren para árboles de rutas de feature

Para features con múltiples rutas anidadas, loadChildren es la opción correcta. Difiere un archivo de rutas completo en su propio chunk, junto con todo lo que ese archivo importa estáticamente. Las rutas hijas todavía pueden usar loadComponent para dividir más, como se ve abajo.

typescript
// app.routes.ts
export const routes: Routes = [
  {
    path: 'lending',
    canActivate: [authGuard],
    loadChildren: () =>
      import('./features/lending/lending.routes')
        .then(m => m.LENDING_ROUTES)
  }
];

// features/lending/lending.routes.ts
export const LENDING_ROUTES: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./application-list/application-list.component')
        .then(m => m.ApplicationListComponent)
  },
  {
    path: ':id',
    loadComponent: () =>
      import('./application-detail/application-detail.component')
        .then(m => m.ApplicationDetailComponent)
  },
  {
    path: ':id/documents',
    loadComponent: () =>
      import('./document-upload/document-upload.component')
        .then(m => m.DocumentUploadComponent)
  }
];

Preloading: Lazy sin el retraso de navegación

La desventaja del lazy loading es que la primera navegación a una ruta lazy dispara una petición de red, lo que causa un breve retraso. Las estrategias de preloading cierran esta brecha cargando chunks en segundo plano después de que la app arranca, sin bloquear el primer render.

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

PreloadAllModules funciona para la mayoría de apps. Para apps más grandes donde el ancho de banda importa, un preloader selectivo personalizado carga solo las rutas de alta prioridad.

typescript
@Injectable({ providedIn: 'root' })
export class PriorityPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
    return route.data?.['preload'] === true ? load() : of(null);
  }
}

// Mark routes that should preload
{
  path: 'applications',
  data: { preload: true },
  loadChildren: () =>
    import('./features/applications/applications.routes')
      .then(m => m.APPLICATIONS_ROUTES)
}

Cómo auditar tu bundle actual

  • Ejecuta ng build --stats-json para generar un archivo stats.json (con el builder application por defecto es un metafile de esbuild que el analizador de bundles de esbuild puede leer)
  • Compila con --source-map y luego ejecuta npx source-map-explorer dist/your-app/browser/main-*.js para visualizar qué contiene
  • Busca cualquier librería de más de 50KB que no se necesite en la pantalla inicial
  • Busca componentes de feature que estén importados directamente en app.config.ts o en módulos cargados eagerly
  • Identifica rutas que la mayoría de usuarios nunca visitan: esas son las primeras candidatas

Errores que anulan el lazy loading

  • Importar un componente lazy directamente en cualquier lugar del árbol cargado eagerly: el import anula la frontera lazy
  • providedIn: 'any' en un servicio que usa una ruta lazy: esto es comportamiento documentado y no un defecto, y aun así atrapa a la gente. Obtienes una instancia por inyector lazy, no un singleton. Usa 'root' cuando el estado tiene que compartirse entre features
  • Librerías de terceros importadas en módulos compartidos que se cargan eagerly: muévelas al feature que las necesita
  • Saltarse el preloading y aceptar que cada primera navegación a una ruta lazy tiene un retraso visible

El lazy loading no es una micro-optimización. A menudo es el camino más rápido de una app lenta a una rápida, medido en horas de trabajo y segundos de mejora.

Ejecuta el análisis de bundle primero. Los números te dirán exactamente por dónde empezar. En cada app que he auditado, dos o tres rutas representan la mayor parte del peso innecesario del bundle principal. Arregla esas y mide de nuevo. La relación esfuerzo-impacto del lazy loading supera cualquier otra técnica de rendimiento que conozco.

Referencias

AngularPerformanceTypeScriptFrontend