This content is only available in Spanish.

Also available in English.

View translation
Frontend

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.

Equipe Blueprintblog23 min
Angular 17+: Enrutamiento Avanzado, Lazy Loading y Precarga

¿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:

typescript
// ❌ 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)

typescript
// 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)

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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

typescript
// 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

typescript
// 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:

typescript
// ❌ 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// ❌ 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// ❌ 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

typescript
// ❌ 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

typescript
// ❌ 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
typescript
// ❌ 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-componentNgRx: Para estado complejo que sobrevive la navegación
  • Actualizaciones en tiempo realWebSockets + RxJS: Para datos que se actualizan en tiempo real
  • Optimistic updatesApollo Client: Para aplicaciones GraphQL con caché inteligente

Matriz de Comparación

CaracterísticaRouter AngularNgRxContext 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


Article tags

Related articles

Get the latest articles delivered to your inbox.

Follow Us: