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@deferen 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:
Dashboard(pantalla principal)MoneyTransferWalletActivity
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:
- 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>. - Disparadores interactivos: Puedes cargar por
viewport,interaction(clic o foco),hover,timer(2s)o cuando el navegador esté enidle. - Estados de experiencia de usuario integrados: Los bloques nativos
@placeholder,@loadingy@errorevitan 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
| Estrategia | Cuándo usarla | Beneficio |
|---|---|---|
loadComponent | Separar vistas y páginas completas de la aplicación | Reduce el tamaño del bundle inicial main.js |
PreloadAllModules | Aplicaciones pequeñas o medianas con conexiones estables | Transiciones instantáneas entre páginas tras la carga inicial |
SelectivePreloadingStrategy | Aplicaciones grandes donde cuidamos el consumo de datos móviles | Precarga únicamente las rutas más frecuentadas o críticas |
@defer (on viewport) | Widgets pesados, gráficas, modales y editores enriquecidos | Pospone la descarga de JavaScript hasta que el componente es visible |
Resumen
Optimizar el rendimiento en Angular moderno requiere un enfoque por capas:
- Usa
loadComponenten tus definiciones de rutas para dividir páginas en fragmentos Standalone. - Configura
withPreloading(SelectivePreloadingStrategy)enprovideRouter()para precargar rutas clave en segundo plano. - 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.
These are my experiences learning and facing my daily challenges working with Angular.
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
Cómo empezar con Standalone Components en Angular (Guía moderna completa)
Aprende a crear aplicaciones en Angular sin módulos usando Standalone Components. Revisa cómo usar bootstrapApplication, app.config.ts, lazy loading con loadComponent y pruebas unitarias con Vitest.
Cómo usar parámetros de ruta y query params en Angular (con Signals e Inputs)
Aprende a pasar y leer parámetros de ruta, query params y parámetros de matriz en Angular usando Signals, withComponentInputBinding y ActivatedRoute.
Preguntas esenciales de entrevista en Angular para desarrolladores Junior y Mid-Level
Una guía práctica de preguntas de entrevista sobre Angular moderno para roles junior y mid-level, cubriendo Signals, Standalone APIs, Control Flow, Vitest y Zoneless Angular.
Cómo probar componentes en Angular usando TestBed
Aprende a probar componentes en Angular con TestBed, manejar componentes hijos y mocks de servicios, y migrar de Karma/Jasmine al moderno Vitest con Standalone y Signals.
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.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.