This content is only available in Spanish.
Also available in English.
Angular 17+: Enrutamiento Avanzado, Lazy Loading y Precarga
¿Tu aplicación Angular carga lento o tiene bundles enormes? Descubre el enrutamiento avanzado en Angular 17+ para transformar tu app. Aprende lazy loading, estrategias de precarga inteligentes y cómo usar standalone components y guards funcionales para una experiencia ultra-rápida. Incluye un ejemplo de e-commerce.

¿Alguna vez te has preguntado por qué tu aplicación Angular tarda en cargar, incluso siendo una SPA? ¿O tal vez has enfrentado problemas de rendimiento con bundles enormes cargándose de una vez?
Hoy, voy a compartir Enrutamiento Avanzado en Angular 17+ - las técnicas y estrategias que transforman aplicaciones lentas en experiencias ultra-rápidas. Al final de este artículo, dominarás lazy loading, estrategias de precarga e implementaciones que tus usuarios adorarán.
¿Qué es el Enrutamiento Moderno en Angular?
El enrutamiento en Angular 17+ no se trata solo de navegar entre páginas - se trata de crear experiencias fluidas, carga inteligente de recursos y optimización automática del rendimiento. Con las nuevas funcionalidades de standalone components y mejoras en el router, tenemos herramientas poderosas para construir aplicaciones más eficientes.
Por Qué Esto Importa
Antes de sumergirnos en la implementación, entendamos el problema que estamos resolviendo:
// ❌ Sin lazy loading - todos los módulos cargados a la vez
const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'users', component: UsersComponent },
{ path: 'products', component: ProductsComponent },
{ path: 'analytics', component: AnalyticsComponent },
// Bundle inicial gigante = 2MB+
];
// ✅ Con lazy loading - carga bajo demanda
const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
},
{
path: 'users',
loadChildren: () => import('./users/users.routes')
},
// Bundle inicial = 300KB, módulos cargados según necesidad
];
Esta transformación reduce el tiempo de carga inicial de segundos a milisegundos, creando una experiencia mucho más responsiva para tus usuarios.
¿Cuándo Usar Enrutamiento Avanzado?
Buenos casos de uso:
- Aplicaciones con múltiples features/módulos distintos
- Dashboards complejos con diferentes secciones
- E-commerce con catálogo, checkout, admin
- Sistemas ERP/CRM con módulos independientes
Cuándo NO usar lazy loading:
- Aplicaciones muy pequeñas (< 5 rutas)
- Features que siempre se acceden juntas
- Cuando la latencia de red es más crítica que el tamaño del bundle
Configuración: Preparando Tu Enrutamiento Moderno
Vamos a construir esto paso a paso. Te mostraré cómo funciona cada pieza y por qué importa cada decisión.
Paso 1: Configuración Base - Standalone Components
Primero, necesitamos configurar la base con standalone components (recomendado en Angular 17+):
Opción 1: Bootstrap con Standalone (Recomendado)
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
// otros providers...
]
});
Opción 2: Módulo Tradicional (Legado)
// app.module.ts
import { RouterModule } from '@angular/router';
@NgModule({
imports: [
RouterModule.forRoot(routes, {
enableTracing: false, // solo para depuración
preloadingStrategy: PreloadAllModules
})
],
// ...
})
export class AppModule { }
Cómo configurar rutas standalone:
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
// Ruta básica standalone
{
path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
},
// Lazy loading de feature completa
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes').then(m => m.dashboardRoutes)
},
// Redirección y wildcard
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: '**', loadComponent: () => import('./not-found/not-found.component') }
];
Por qué esta configuración funciona tan bien:
- Inicialización más rápida: Componentes standalone reducen overhead de módulos
- Tree-shaking mejorado: Solo el código usado se incluye en el bundle
- Desarrollo más simple: Menos boilerplate, enfoque en lo que importa
Paso 2: Implementando Lazy Loading Inteligente
Ahora vamos a implementar lazy loading de forma estratégica, explicando cada decisión:
2.1: Lazy Loading de Componentes Simples
Primero, vamos a crear la estructura básica para componentes standalone:
// feature/product/product.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-product',
standalone: true,
imports: [CommonModule, RouterModule],
template: `
<div class="product-container">
<h2>Catálogo de Productos</h2>
<router-outlet></router-outlet>
</div>
`
})
export class ProductComponent { }
2.2: Configuración de Rutas con Lazy Loading
Ahora vamos a implementar lazy loading con diferentes estrategias:
// app.routes.ts
import { Routes } from '@angular/router';
import { inject } from '@angular/core';
import { AuthGuard } from './guards/auth.guard';
export const routes: Routes = [
// Componente standalone con lazy loading
{
path: 'profile',
canActivate: [() => inject(AuthGuard).canActivate()],
loadComponent: () => import('./profile/profile.component')
.then(m => m.ProfileComponent)
},
// Feature module completo con subrutas
{
path: 'products',
loadChildren: () => import('./products/products.routes')
.then(m => m.productRoutes),
data: { preload: true } // Marca para precarga
},
// Carga condicional basada en permisos
{
path: 'admin',
canMatch: [() => inject(AuthGuard).hasAdminRole()],
loadChildren: () => import('./admin/admin.routes')
}
];
¿Por qué esta implementación?
- Seguridad: Guards verifican permisos antes de la carga
- Rendimiento: Cada feature carga solo cuando es necesaria
- Flexibilidad: Diferentes estrategias para diferentes necesidades
2.3: Creando Rutas de Feature
Vamos a crear un sistema de rutas jerárquico para features complejas:
// products/products.routes.ts
import { Routes } from '@angular/router';
export const productRoutes: Routes = [
{
path: '',
loadComponent: () => import('./product-layout.component')
.then(m => m.ProductLayoutComponent),
children: [
{
path: '',
loadComponent: () => import('./product-list/product-list.component')
},
{
path: 'category/:id',
loadComponent: () => import('./product-category/product-category.component'),
data: { preload: true }
},
{
path: 'details/:id',
loadComponent: () => import('./product-details/product-details.component'),
resolve: {
product: () => inject(ProductService).getProduct(
inject(ActivatedRoute).snapshot.params['id']
)
}
}
]
}
];
Diferencias importantes:
- Rutas anidadas: Estructura jerárquica clara para UX consistente
- Resolvers: Datos cargados antes de la navegación, evitando estados de carga
- Data binding: Metadatos para control de precarga y caché
2.4: Implementación con Guards Funcionales
// guards/auth.guard.ts
import { inject } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
export const authGuard = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (authService.isAuthenticated()) {
return true;
}
return router.parseUrl('/login');
};
// Usando el guard
{
path: 'dashboard',
canActivate: [authGuard],
loadChildren: () => import('./dashboard/dashboard.routes')
}
Guards funcionales explicados:
- Más limpios: Sin clases, solo funciones puras
- Mejor testabilidad: Fácil de mockear y testear
- Rendimiento: Menos overhead de instanciación
Paso 3: Estrategias de Precarga Inteligentes
// strategies/smart-preload.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, timer } from 'rxjs';
import { mergeMap } from 'rxjs/operators';
@Injectable()
export class SmartPreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Precarga solo si está marcado en los data
if (route.data?.['preload']) {
// Espera 2 segundos después de la carga inicial
return timer(2000).pipe(
mergeMap(() => load())
);
}
return of(null);
}
}
// Configuración en el provider
provideRouter(routes, withPreloading(SmartPreloadStrategy))
Cómo todas las piezas trabajan juntas: el sistema carga la ruta inicial instantáneamente, luego identifica rutas marcadas para precarga y las carga en segundo plano después de un delay, garantizando que los recursos críticos no sean bloqueados.
Ejemplo Complejo: E-commerce con Micro-frontend
Vamos a construir algo más realista - un e-commerce completo que demuestra uso avanzado:
Entendiendo el Problema
Antes de saltar al código, entendamos qué estamos construyendo:
// ❌ Enfoque ingenuo - todo cargado junto
const routes: Routes = [
{ path: 'products', component: ProductsComponent },
{ path: 'cart', component: CartComponent },
{ path: 'checkout', component: CheckoutComponent },
{ path: 'admin', component: AdminComponent },
// Bundle inicial = 3.5MB, tiempo de carga = 8s
];
// ✅ Nuestro enfoque - carga estratégica
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./features/catalog/catalog.routes'),
data: { preload: true } // Probablemente será accedido
},
{
path: 'checkout',
loadChildren: () => import('./features/checkout/checkout.routes')
// Cargado solo cuando es necesario
}
// Bundle inicial = 280KB, carga incremental
];
Implementación Paso a Paso
Fase 1: Estructura Base del E-commerce
// app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
import { SmartPreloadStrategy } from './strategies/smart-preload.strategy';
export const routes: Routes = [
// Landing page - carga inmediata
{
path: '',
loadComponent: () => import('./features/home/home.component')
},
// Catálogo - precarga habilitada (alta probabilidad de acceso)
{
path: 'products',
loadChildren: () => import('./features/catalog/catalog.routes'),
data: {
preload: true,
priority: 'high'
}
},
// Carrito - carga bajo demanda
{
path: 'cart',
loadChildren: () => import('./features/cart/cart.routes'),
data: { preload: false }
},
// Checkout - carga protegida
{
path: 'checkout',
canActivate: [authGuard],
loadChildren: () => import('./features/checkout/checkout.routes'),
data: {
requiresAuth: true,
preloadOnAuth: true // Precarga cuando el usuario se autentica
}
},
// Admin - acceso restringido
{
path: 'admin',
canMatch: [() => inject(AuthService).hasRole('admin')],
loadChildren: () => import('./features/admin/admin.routes')
}
];
Desglosando esto:
- Priorización inteligente: Recursos más usados tienen prioridad
- Seguridad por capas: Guards en diferentes niveles según necesidad
- Precarga condicional: Basada en contexto del usuario
Fase 2: Módulo de Catálogo con Subrutas Optimizadas
// features/catalog/catalog.routes.ts
import { Routes } from '@angular/router';
import { productResolver } from './resolvers/product.resolver';
export const catalogRoutes: Routes = [
{
path: '',
loadComponent: () => import('./catalog-layout.component'),
children: [
// Lista de productos - vista principal
{
path: '',
loadComponent: () => import('./views/product-list/product-list.component'),
data: {
title: 'Productos',
description: 'Catálogo completo de productos'
}
},
// Categoría específica - precarga activada
{
path: 'category/:slug',
loadComponent: () => import('./views/category/category.component'),
resolve: {
category: (route: ActivatedRouteSnapshot) =>
inject(CategoryService).getBySlug(route.params['slug'])
},
data: { preload: true }
},
// Detalles del producto - resolvers para UX
{
path: 'product/:id',
loadComponent: () => import('./views/product-details/product-details.component'),
resolve: {
product: productResolver,
recommendations: (route: ActivatedRouteSnapshot) =>
inject(RecommendationService).getFor(route.params['id'])
}
},
// Búsqueda - componente ligero
{
path: 'search',
loadComponent: () => import('./views/search/search.component'),
data: {
preload: true,
cache: true // Cachear resultados
}
}
]
}
];
Por qué esta integración funciona:
- Resolvers inteligentes: Datos cargados antes de la navegación
- Estrategia de caché: Evita requests innecesarias
- Estructura jerárquica: Reutilización de layout y estado
Fase 3: Implementación de Checkout Avanzado
// features/checkout/checkout.routes.ts
import { Routes } from '@angular/router';
import { cartGuard } from './guards/cart.guard';
import { stepGuard } from './guards/step.guard';
export const checkoutRoutes: Routes = [
{
path: '',
canActivate: [cartGuard], // Verifica si hay items en el carrito
loadComponent: () => import('./checkout-layout.component'),
children: [
// Proceso de checkout en pasos
{
path: '',
redirectTo: 'shipping',
pathMatch: 'full'
},
// Paso 1: Dirección de entrega
{
path: 'shipping',
loadComponent: () => import('./steps/shipping/shipping.component'),
data: { step: 1, title: 'Dirección de Entrega' }
},
// Paso 2: Método de pago
{
path: 'payment',
canActivate: [stepGuard(1)], // Solo accede si el paso anterior está completo
loadComponent: () => import('./steps/payment/payment.component'),
data: { step: 2, title: 'Pago' }
},
// Paso 3: Confirmación
{
path: 'review',
canActivate: [stepGuard(2)],
loadComponent: () => import('./steps/review/review.component'),
resolve: {
orderSummary: () => inject(CheckoutService).getOrderSummary()
},
data: { step: 3, title: 'Revisar Pedido' }
},
// Confirmación final
{
path: 'success/:orderId',
loadComponent: () => import('./success/success.component'),
resolve: {
order: (route: ActivatedRouteSnapshot) =>
inject(OrderService).getById(route.params['orderId'])
}
}
]
}
];
Por qué esta arquitectura es poderosa:
- Flujo controlado: Guards garantizan progresión correcta
- Estado preservado: Layout compartido mantiene contexto
- UX optimizada: Resolvers evitan estados de carga durante navegación
Patrón Avanzado: Micro-frontends con Module Federation
Ahora vamos a explorar un patrón avanzado que demuestra uso nivel maestro.
El Problema con Monolitos de Frontend
// ❌ Limitaciones del enfoque simple
const routes: Routes = [
{ path: 'products', loadChildren: () => import('./products/products.module') },
{ path: 'orders', loadChildren: () => import('./orders/orders.module') },
{ path: 'analytics', loadChildren: () => import('./analytics/analytics.module') }
// Todas las features en el mismo repositorio = acoplamiento de deploy
];
Por qué esto se vuelve problemático:
- Equipos diferentes no pueden deployar independientemente
- Builds largos incluso para cambios pequeños
- Dependencias compartidas causan conflictos de versión
Construyendo la Solución con Module Federation
Etapa 1: Configuración de la Aplicación Shell
// shell-app/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
'products-mf': 'products@http://localhost:4201/remoteEntry.js',
'orders-mf': 'orders@http://localhost:4202/remoteEntry.js',
'analytics-mf': 'analytics@http://localhost:4203/remoteEntry.js'
},
shared: {
'@angular/core': { singleton: true, strictVersion: true },
'@angular/common': { singleton: true, strictVersion: true },
'@angular/router': { singleton: true, strictVersion: true }
}
})
]
};
// shell-app/src/app/app.routes.ts
export const routes: Routes = [
{
path: 'products',
loadChildren: () => import('products-mf/Routes').then(m => m.routes),
data: {
microfrontend: 'products',
fallback: () => import('./fallbacks/products-fallback.component')
}
},
{
path: 'orders',
loadChildren: () => import('orders-mf/Routes').then(m => m.routes)
.catch(() => import('./fallbacks/orders-fallback.component')),
data: { microfrontend: 'orders' }
},
{
path: 'analytics',
loadChildren: () => import('analytics-mf/Routes').then(m => m.routes),
canLoad: [() => inject(FeatureToggleService).isEnabled('analytics')],
data: { microfrontend: 'analytics' }
}
];
Module Federation profundo:
- Qué hace: Permite cargar código de aplicaciones separadas en runtime
- Por qué es poderoso: Deploy independiente + dependencias compartidas optimizadas
- Cuándo usar: Equipos grandes, features independientes, releases frecuentes
Etapa 2: Micro-frontend de Productos
// products-mf/webpack.config.js
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'products',
filename: 'remoteEntry.js',
exposes: {
'./Routes': './src/app/app.routes.ts'
},
shared: {
'@angular/core': { singleton: true },
'@angular/common': { singleton: true },
'@angular/router': { singleton: true }
}
})
]
};
// products-mf/src/app/app.routes.ts
export const routes: Routes = [
{
path: '',
loadComponent: () => import('./product-shell.component'),
children: [
{
path: '',
loadComponent: () => import('./views/catalog/catalog.component')
},
{
path: 'details/:id',
loadComponent: () => import('./views/details/details.component'),
resolve: {
product: (route: ActivatedRouteSnapshot) =>
inject(ProductService).getProduct(route.params['id'])
}
}
]
}
];
Patrones de integración:
- Comunicación vía Router: Estado compartido a través de query params
- Event Bus: Custom events para comunicación cross-microfrontend
- Shared Services: Inyectados vía dependency injection
Etapa 3: Manejo de Errores y Fallbacks
// shell-app/src/app/services/microfrontend-loader.service.ts
@Injectable({
providedIn: 'root'
})
export class MicrofrontendLoaderService {
private failedLoads = new Set<string>();
async loadMicrofrontend(name: string): Promise<any> {
if (this.failedLoads.has(name)) {
return this.loadFallback(name);
}
try {
const module = await import(`${name}-mf/Routes`);
return module.routes;
} catch (error) {
console.warn(`Failed to load ${name} microfrontend:`, error);
this.failedLoads.add(name);
return this.loadFallback(name);
}
}
private async loadFallback(name: string) {
switch (name) {
case 'products':
return import('./fallbacks/products-fallback.routes');
case 'orders':
return import('./fallbacks/orders-fallback.routes');
default:
return import('./fallbacks/generic-fallback.routes');
}
}
}
// Ruta con fallback automático
{
path: 'products',
loadChildren: () => inject(MicrofrontendLoaderService).loadMicrofrontend('products'),
data: { microfrontend: 'products' }
}
Por qué esta arquitectura es robusta:
- Resiliente a fallos: Fallbacks automáticos cuando micro-frontends no están disponibles
- Deploy independiente: Cada equipo puede deployar sin afectar a otros
- Escalabilidad: Nuevos micro-frontends pueden añadirse sin rebuild
Enrutamiento con TypeScript Avanzado
Para usuarios de TypeScript, aquí está cómo hacer todo type-safe:
Configurando Types Robustos
// types/routing.ts
import { Data, Route } from '@angular/router';
export interface RouteData extends Data {
title?: string;
description?: string;
preload?: boolean;
priority?: 'low' | 'normal' | 'high';
requiresAuth?: boolean;
roles?: string[];
microfrontend?: string;
cache?: boolean;
fallback?: () => Promise<any>;
}
export interface AppRoute extends Omit<Route, 'data'> {
data?: RouteData;
children?: AppRoute[];
}
// Utility types para resolvers
export interface ProductResolverData {
product: Product;
recommendations: Product[];
reviews: Review[];
}
export type RouteResolvers<T = any> = {
[K in keyof T]: (route: ActivatedRouteSnapshot) => Observable<T[K]> | Promise<T[K]> | T[K];
};
Beneficios de type safety:
- Autocomplete: IDE sugiere propiedades disponibles
- Compile-time checks: Errores detectados antes del runtime
Implementación con Tipado Adecuado
// routes/typed-routes.ts
import { inject } from '@angular/core';
import { AppRoute, ProductResolverData, RouteResolvers } from '../types/routing';
import { ProductService } from '../services/product.service';
const productResolvers: RouteResolvers<ProductResolverData> = {
product: (route) => inject(ProductService).getById(route.params['id']),
recommendations: (route) => inject(ProductService).getRecommendations(route.params['id']),
reviews: (route) => inject(ProductService).getReviews(route.params['id'])
};
export const productRoutes: AppRoute[] = [
{
path: 'details/:id',
loadComponent: () => import('./product-details.component'),
resolve: productResolvers,
data: {
title: 'Detalles del Producto',
preload: true,
priority: 'high',
cache: true
}
}
];
Patrones TypeScript Avanzados
// guards/typed-guards.ts
import { CanActivateFn, CanMatchFn } from '@angular/router';
// Guard factory con types
export function createRoleGuard(roles: string[]): CanActivateFn {
return () => {
const authService = inject(AuthService);
return authService.hasAnyRole(roles);
};
}
// Guard compuesto
export function createCompositeGuard(
guards: CanActivateFn[]
): CanActivateFn {
return (route, state) => {
return guards.every(guard => guard(route, state));
};
}
// Uso tipado
const adminGuard = createRoleGuard(['admin', 'super-admin']);
const secureGuard = createCompositeGuard([authGuard, adminGuard]);
export const adminRoutes: AppRoute[] = [
{
path: 'admin',
canActivate: [secureGuard],
loadChildren: () => import('./admin/admin.routes'),
data: {
requiresAuth: true,
roles: ['admin'],
title: 'Panel Administrativo'
}
}
];
Patrones Avanzados y Mejores Prácticas
1. Strategy Pattern para Precarga
Qué resuelve: Diferentes comportamientos de precarga basados en contexto
Cómo funciona: Interfaz común con implementaciones específicas
// strategies/preload-strategies.ts
interface PreloadStrategy {
shouldPreload(route: Route): boolean;
getDelay(route: Route): number;
}
class NetworkAwareStrategy implements PreloadStrategy {
shouldPreload(route: Route): boolean {
// @ts-ignore - navigator.connection es experimental
const connection = navigator.connection;
if (connection?.effectiveType === '4g' && route.data?.['priority'] === 'high') {
return true;
}
return route.data?.['preload'] === true && connection?.effectiveType !== 'slow-2g';
}
getDelay(route: Route): number {
// @ts-ignore
const connection = navigator.connection;
return connection?.effectiveType === '4g' ? 500 : 2000;
}
}
class UserBehaviorStrategy implements PreloadStrategy {
private readonly analytics = inject(AnalyticsService);
shouldPreload(route: Route): boolean {
const path = route.path;
const userProbability = this.analytics.getNavigationProbability(path);
return userProbability > 0.7; // 70% de probabilidad de navegación
}
getDelay(): number {
return 1000;
}
}
// Implementación de la estrategia compuesta
@Injectable()
export class SmartPreloadStrategy implements PreloadingStrategy {
private strategies: PreloadStrategy[] = [
new NetworkAwareStrategy(),
new UserBehaviorStrategy()
];
preload(route: Route, load: () => Observable<any>): Observable<any> {
const shouldPreload = this.strategies.some(strategy =>
strategy.shouldPreload(route)
);
if (!shouldPreload) {
return of(null);
}
const delay = Math.min(
...this.strategies.map(strategy => strategy.getDelay(route))
);
return timer(delay).pipe(mergeMap(() => load()));
}
}
Cuándo usar: Aplicaciones con requisitos complejos de precarga basados en contexto
2. Router State Management
El problema: Estado perdido durante la navegación
La solución: Store centralizado para estado de navegación
// services/router-state.service.ts
@Injectable({
providedIn: 'root'
})
export class RouterStateService {
private state = new BehaviorSubject<any>({});
private history: string[] = [];
constructor(private router: Router) {
this.router.events.pipe(
filter(event => event instanceof NavigationEnd),
map(event => (event as NavigationEnd).url)
).subscribe(url => {
this.history.push(url);
// Mantener solo los últimos 10
if (this.history.length > 10) {
this.history.shift();
}
});
}
setState(key: string, value: any): void {
const currentState = this.state.value;
this.state.next({ ...currentState, [key]: value });
}
getState(key: string): any {
return this.state.value[key];
}
canGoBack(): boolean {
return this.history.length > 1;
}
goBack(): void {
if (this.canGoBack()) {
const previousUrl = this.history[this.history.length - 2];
this.router.navigateByUrl(previousUrl);
}
}
}
// Uso en componentes
@Component({
template: `
<button (click)="goBack()" [disabled]="!canGoBack()">
Volver
</button>
`
})
export class NavigationComponent {
private routerState = inject(RouterStateService);
get canGoBack() {
return this.routerState.canGoBack();
}
goBack() {
this.routerState.goBack();
}
}
Beneficios: Estado preservado, navegación inteligente, mejor UX
3. Route Data Caching
Caso de uso: Evitar re-requests innecesarias en datos que no cambian frecuentemente
// services/route-cache.service.ts
@Injectable({
providedIn: 'root'
})
export class RouteCacheService {
private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();
set<T>(key: string, data: T, ttlMinutes: number = 5): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl: ttlMinutes * 60 * 1000
});
}
get<T>(key: string): T | null {
const cached = this.cache.get(key);
if (!cached) {
return null;
}
if (Date.now() - cached.timestamp > cached.ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
invalidate(pattern?: string): void {
if (!pattern) {
this.cache.clear();
return;
}
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
// Resolver con caché
export const cachedProductResolver = (route: ActivatedRouteSnapshot) => {
const cache = inject(RouteCacheService);
const productService = inject(ProductService);
const productId = route.params['id'];
const cacheKey = `product-${productId}`;
// Verifica caché primero
const cached = cache.get(cacheKey);
if (cached) {
return of(cached);
}
// Si no hay caché, busca y almacena
return productService.getById(productId).pipe(
tap(product => cache.set(cacheKey, product, 10)) // Caché por 10 minutos
);
};
Señales de alerta: Caché demasiado agresivo puede mostrar datos desactualizados
4. Progressive Enhancement
El concepto: Aplicación funciona incluso con JavaScript deshabilitado
// services/progressive-enhancement.service.ts
@Injectable({
providedIn: 'root'
})
export class ProgressiveEnhancementService {
private supportsHistory = typeof window !== 'undefined' &&
window.history && window.history.pushState;
enhanceNavigation(): void {
if (!this.supportsHistory) {
return; // Fallback a navegación tradicional
}
// Intercepta clicks en links y convierte a navegación SPA
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const link = target.closest('a[href]') as HTMLAnchorElement;
if (link && this.shouldEnhance(link)) {
event.preventDefault();
inject(Router).navigateByUrl(link.href);
}
});
}
private shouldEnhance(link: HTMLAnchorElement): boolean {
// No enhance links externos
if (link.hostname !== window.location.hostname) {
return false;
}
// No enhance descargas
if (link.download) {
return false;
}
return true;
}
}
Trade-offs: Complejidad adicional vs mejor accesibilidad y SEO
Trampas Comunes a Evitar
1. Over-Engineering del Lazy Loading
El problema: Lazy loading excesivo para aplicaciones pequeñas
// ❌ No hagas esto - lazy loading innecesario para apps pequeñas
const routes: Routes = [
{
path: 'simple-page',
loadComponent: () => import('./simple-page.component') // Componente de 2KB
}
];
// ✅ Haz esto - directo para componentes simples
const routes: Routes = [
{
path: 'simple-page',
component: SimplePageComponent // Componente ya cargado
}
];
Por qué esto importa: Overhead de lazy loading puede ser mayor que el beneficio para componentes pequeños
2. Resolvers Bloqueantes
Error común: Resolvers que tardan demasiado en resolver
Por qué sucede: Requests lentas o secuenciales innecesarias
// ❌ Evita esto - resolvers que bloquean navegación
export const slowResolver = (route: ActivatedRouteSnapshot) => {
const service = inject(SlowService);
// Request lenta que bloquea navegación
return service.getSlowData(route.params['id']).pipe(
delay(5000) // ¡5 segundos de bloqueo!
);
};
// ✅ Solución - carga asíncrona en el componente
@Component({
template: `
<div *ngIf="loading">Cargando...</div>
<div *ngIf="data">{{ data | json }}</div>
`
})
export class FastComponent implements OnInit {
loading = true;
data: any;
ngOnInit() {
const id = inject(ActivatedRoute).snapshot.params['id'];
inject(SlowService).getSlowData(id).subscribe(data => {
this.data = data;
this.loading = false;
});
}
}
Prevención: Usa resolvers solo para datos críticos, estados de carga para el resto
3. Memory Leaks en Módulos Lazy
La trampa: Subscriptions no canceladas en módulos lazy
// ❌ Evita esto - subscription que gotea memoria
@Component({
template: `<div>{{ data$ | async }}</div>`
})
export class LeakyComponent implements OnInit {
data$!: Observable<any>;
ngOnInit() {
// Subscription que nunca se cancela
this.service.getData().subscribe(data => {
// Procesamiento que puede gotear
});
}
}
// ✅ Solución - limpieza automática
@Component({
template: `<div>{{ data$ | async }}</div>`
})
export class CleanComponent implements OnDestroy {
private destroy$ = new Subject<void>();
data$!: Observable<any>;
ngOnInit() {
this.data$ = this.service.getData().pipe(
takeUntil(this.destroy$)
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Señales de alerta: Uso de memoria creciendo en navegación repetida
Cuándo NO Usar Lazy Loading
No uses lazy loading cuando:
- Aplicaciones muy pequeñas: Menos de 5 rutas principales
- Features siempre usadas juntas: Dashboard con widgets interdependientes
- Latencia crítica: Aplicaciones donde cada milisegundo importa
// ❌ Overkill para escenarios simples
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () => import('./dashboard/dashboard.routes') // Overhead innecesario
}
];
// ✅ Solución simple es mejor
const routes: Routes = [
{
path: 'dashboard',
component: DashboardComponent, // Carga directa
children: [
{ path: 'overview', component: OverviewComponent },
{ path: 'stats', component: StatsComponent }
]
}
];
Framework de decisión: Usa lazy loading cuando bundle > 1MB o features independientes
Enrutamiento vs Redux/NgRx
Cuándo el Enrutamiento Brilla
El enrutamiento es excelente para:
- Estado basado en URL: Filtros, paginación, navegación
- Navegación simple: Entre páginas y features
- SEO y deep linking: URLs amigables y compartibles
Cuándo Considerar Alternativas
Considera state management cuando necesitas:
- Estado complejo cross-component → NgRx: Para estado complejo que sobrevive la navegación
- Actualizaciones en tiempo real → WebSockets + RxJS: Para datos que se actualizan en tiempo real
- Optimistic updates → Apollo Client: Para aplicaciones GraphQL con caché inteligente
Matriz de Comparación
| Característica | Router Angular | NgRx | Context API |
|---|---|---|---|
| Estado en URL | ✅ Excelente | ❌ No soporta | ❌ No soporta |
| Rendimiento | ✅ Lazy loading | ⚠️ Overhead inicial | ✅ Ligero |
| Complejidad | ✅ Simple | ❌ Curva de aprendizaje | ✅ Simple |
| Debug | ✅ URL debugging | ✅ DevTools | ⚠️ Limitado |
| Time travel | ❌ No soporta | ✅ Excelente | ❌ No soporta |
Conclusión
El enrutamiento en Angular 17+ es una herramienta poderosa que puede transformar aplicaciones lentas en experiencias ultra-rápidas. Trae lazy loading inteligente, precarga estratégica y arquitecturas escalables a tus SPAs.
Conclusiones clave:
- Lazy loading es esencial: Pero solo para features independientes y aplicaciones grandes
- Precarga estratégica: Usa datos de comportamiento del usuario para optimizar carga
- Guards funcionales: Más limpios y testables que clases tradicionales
- TypeScript hasta el final: Type safety evita bugs en runtime y mejora DX
La próxima vez que construyas una aplicación Angular, recuerda estas estrategias. Tus usuarios notarán la diferencia en velocidad, y tu equipo te agradecerá por la arquitectura limpia.
Próximos pasos:
- Analiza tu aplicación actual e identifica oportunidades de lazy loading
- Implementa una estrategia de precarga basada en analytics
- Configura type safety completo en tus rutas
¿Ya has implementado lazy loading en tus proyectos? ¿Qué patrones han funcionado mejor para ti? ¡Comparte tu experiencia en los comentarios!
Si esta guía te ayudó a dominar el enrutamiento en Angular, sígueme para más patrones y mejores prácticas avanzadas! 🚀
Recursos
- Documentación de Angular Router
- Lazy Loading Feature Modules
- Estrategias de Precarga
- Module Federation con Angular



