Este conteúdo está disponível apenas em Inglês.

Também está disponível em Português.

Ver tradução
Frontend

Angular 17+ Advanced Routing: Lazy Loading & Preload

Optimize your Angular 17+ application's performance with advanced routing techniques. This guide covers how to implement intelligent lazy loading, smart preload strategies, standalone components, and functional guards. Transform slow applications into ultra-fast experiences, reducing initial load times and enhancing user satisfaction.

Equipe Blueprintblog23 min
Angular 17+ Advanced Routing: Lazy Loading & Preload

Have you ever wondered why your Angular application takes so long to load, even though it's a SPA? Or maybe you've faced performance issues with huge bundles being loaded all at once?

Today, I'll share Advanced Routing in Angular 17+ - the techniques and strategies that transform slow applications into ultra-fast experiences. By the end of this article, you'll master lazy loading, preload strategies, and implementations your users will love.

What is Modern Routing in Angular?

Routing in Angular 17+ isn't just about navigating between pages - it's about creating fluid experiences, intelligent resource loading, and automatic performance optimization. With the new standalone components features and router improvements, we have powerful tools to build more efficient applications.

Why This Matters

Before diving into implementation, let's understand the problem we're solving:

typescript
// ❌ Without lazy loading - all modules loaded at once
const routes: Routes = [
  { path: 'dashboard', component: DashboardComponent },
  { path: 'users', component: UsersComponent },
  { path: 'products', component: ProductsComponent },
  { path: 'analytics', component: AnalyticsComponent },
  // Initial bundle = 2MB+
];

// ✅ With lazy loading - on-demand loading
const routes: Routes = [
  { 
    path: 'dashboard', 
    loadComponent: () => import('./dashboard/dashboard.component')
  },
  { 
    path: 'users', 
    loadChildren: () => import('./users/users.routes')
  },
  // Initial bundle = 300KB, modules loaded as needed
];

This transformation reduces initial load time from seconds to milliseconds, creating a much more responsive experience for your users.

When to Use Advanced Routing?

Good use cases:

  • Applications with multiple distinct features/modules
  • Complex dashboards with different sections
  • E-commerce with catalog, checkout, admin
  • ERP/CRM systems with independent modules

When NOT to use lazy loading:

  • Very small applications (< 5 routes)
  • Features that are always accessed together
  • When network latency is more critical than bundle size

Configuration: Setting Up Your Modern Routing

Let's build this step by step. I'll show you how each piece works and why each decision matters.

Step 1: Base Setup - Standalone Components

First, we need to set up the base with standalone components (recommended in Angular 17+):

Option 1: Bootstrap with Standalone (Recommended)

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),
    // other providers...
  ]
});

Option 2: Traditional Module (Legacy)

typescript
// app.module.ts
import { RouterModule } from '@angular/router';

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      enableTracing: false, // only for debugging
      preloadingStrategy: PreloadAllModules
    })
  ],
  // ...
})
export class AppModule { }

How to configure standalone routes:

typescript
// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  // Basic standalone route
  {
    path: '',
    loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
  },
  
  // Lazy loading complete feature
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.routes').then(m => m.dashboardRoutes)
  },
  
  // Redirect and wildcard
  { path: '', redirectTo: '/home', pathMatch: 'full' },
  { path: '**', loadComponent: () => import('./not-found/not-found.component') }
];

Why this configuration works so well:

  • Faster initialization: Standalone components reduce module overhead
  • Better tree-shaking: Only used code is included in the bundle
  • Simpler development: Less boilerplate, focus on what matters

Step 2: Implementing Intelligent Lazy Loading

Now let's implement lazy loading strategically, explaining each decision:

2.1: Lazy Loading Simple Components

First, let's create the basic structure for standalone components:

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>Product Catalog</h2>
      <router-outlet></router-outlet>
    </div>
  `
})
export class ProductComponent { }

2.2: Route Configuration with Lazy Loading

Now let's implement lazy loading with different strategies:

typescript
// app.routes.ts
import { Routes } from '@angular/router';
import { inject } from '@angular/core';
import { AuthGuard } from './guards/auth.guard';

export const routes: Routes = [
  // Standalone component with lazy loading
  {
    path: 'profile',
    canActivate: [() => inject(AuthGuard).canActivate()],
    loadComponent: () => import('./profile/profile.component')
      .then(m => m.ProfileComponent)
  },
  
  // Complete feature module with subroutes
  {
    path: 'products',
    loadChildren: () => import('./products/products.routes')
      .then(m => m.productRoutes),
    data: { preload: true } // Mark for preload
  },
  
  // Conditional loading based on permissions
  {
    path: 'admin',
    canMatch: [() => inject(AuthGuard).hasAdminRole()],
    loadChildren: () => import('./admin/admin.routes')
  }
];

Why this implementation?

  • Security: Guards verify permissions before loading
  • Performance: Each feature loads only when needed
  • Flexibility: Different strategies for different needs

2.3: Creating Feature Routes

Let's create a hierarchical route system for complex features:

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']
          )
        }
      }
    ]
  }
];

Important differences:

  • Nested routes: Clear hierarchical structure for consistent UX
  • Resolvers: Data loaded before navigation, avoiding loading states
  • Data binding: Metadata for preload and cache control

2.4: Implementation with Functional Guards

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');
};

// Using the guard
{
  path: 'dashboard',
  canActivate: [authGuard],
  loadChildren: () => import('./dashboard/dashboard.routes')
}

Functional guards explained:

  • Cleaner: No classes, just pure functions
  • Better testability: Easy to mock and test
  • Performance: Less instantiation overhead

Step 3: Intelligent Preloading Strategies

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> {
    // Preload only if marked in data
    if (route.data?.['preload']) {
      // Wait 2 seconds after initial load
      return timer(2000).pipe(
        mergeMap(() => load())
      );
    }
    
    return of(null);
  }
}

// Configuration in provider
provideRouter(routes, withPreloading(SmartPreloadStrategy))

How all pieces work together: the system loads the initial route instantly, then identifies routes marked for preload and loads them in the background after a delay, ensuring critical resources aren't blocked.

Complex Example: E-commerce with Micro-frontend

Let's build something more realistic - a complete e-commerce that demonstrates advanced usage:

Understanding the Problem

Before jumping to code, let's understand what we're building:

typescript
// ❌ Naive approach - everything loaded together
const routes: Routes = [
  { path: 'products', component: ProductsComponent },
  { path: 'cart', component: CartComponent },
  { path: 'checkout', component: CheckoutComponent },
  { path: 'admin', component: AdminComponent },
  // Initial bundle = 3.5MB, load time = 8s
];

// ✅ Our approach - strategic loading
const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('./features/catalog/catalog.routes'),
    data: { preload: true } // Likely to be accessed
  },
  {
    path: 'checkout',
    loadChildren: () => import('./features/checkout/checkout.routes')
    // Loaded only when needed
  }
  // Initial bundle = 280KB, incremental loading
];

Step-by-Step Implementation

Phase 1: E-commerce Base Structure

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 - immediate loading
  {
    path: '',
    loadComponent: () => import('./features/home/home.component')
  },
  
  // Catalog - preload enabled (high access probability)
  {
    path: 'products',
    loadChildren: () => import('./features/catalog/catalog.routes'),
    data: { 
      preload: true,
      priority: 'high'
    }
  },
  
  // Cart - on-demand loading
  {
    path: 'cart',
    loadChildren: () => import('./features/cart/cart.routes'),
    data: { preload: false }
  },
  
  // Checkout - protected loading
  {
    path: 'checkout',
    canActivate: [authGuard],
    loadChildren: () => import('./features/checkout/checkout.routes'),
    data: { 
      requiresAuth: true,
      preloadOnAuth: true // Preload when user authenticates
    }
  },
  
  // Admin - restricted access
  {
    path: 'admin',
    canMatch: [() => inject(AuthService).hasRole('admin')],
    loadChildren: () => import('./features/admin/admin.routes')
  }
];

Breaking this down:

  • Intelligent prioritization: Most-used resources have priority
  • Security by layers: Guards at different levels as needed
  • Conditional preload: Based on user context

Phase 2: Catalog Module with Optimized Subroutes

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: [
      // Product list - main view
      {
        path: '',
        loadComponent: () => import('./views/product-list/product-list.component'),
        data: { 
          title: 'Products',
          description: 'Complete product catalog'
        }
      },
      
      // Specific category - preload enabled
      {
        path: 'category/:slug',
        loadComponent: () => import('./views/category/category.component'),
        resolve: {
          category: (route: ActivatedRouteSnapshot) => 
            inject(CategoryService).getBySlug(route.params['slug'])
        },
        data: { preload: true }
      },
      
      // Product details - resolvers for UX
      {
        path: 'product/:id',
        loadComponent: () => import('./views/product-details/product-details.component'),
        resolve: {
          product: productResolver,
          recommendations: (route: ActivatedRouteSnapshot) =>
            inject(RecommendationService).getFor(route.params['id'])
        }
      },
      
      // Search - lightweight component
      {
        path: 'search',
        loadComponent: () => import('./views/search/search.component'),
        data: { 
          preload: true,
          cache: true // Cache results
        }
      }
    ]
  }
];

Why this integration works:

  • Intelligent resolvers: Data loaded before navigation
  • Cache strategy: Avoids unnecessary requests
  • Hierarchical structure: Layout and state reuse

Phase 3: Advanced Checkout Implementation

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], // Check if there are items in cart
    loadComponent: () => import('./checkout-layout.component'),
    children: [
      // Checkout process in steps
      {
        path: '',
        redirectTo: 'shipping',
        pathMatch: 'full'
      },
      
      // Step 1: Shipping address
      {
        path: 'shipping',
        loadComponent: () => import('./steps/shipping/shipping.component'),
        data: { step: 1, title: 'Shipping Address' }
      },
      
      // Step 2: Payment method
      {
        path: 'payment',
        canActivate: [stepGuard(1)], // Only access if previous step is complete
        loadComponent: () => import('./steps/payment/payment.component'),
        data: { step: 2, title: 'Payment' }
      },
      
      // Step 3: Confirmation
      {
        path: 'review',
        canActivate: [stepGuard(2)],
        loadComponent: () => import('./steps/review/review.component'),
        resolve: {
          orderSummary: () => inject(CheckoutService).getOrderSummary()
        },
        data: { step: 3, title: 'Review Order' }
      },
      
      // Final confirmation
      {
        path: 'success/:orderId',
        loadComponent: () => import('./success/success.component'),
        resolve: {
          order: (route: ActivatedRouteSnapshot) =>
            inject(OrderService).getById(route.params['orderId'])
        }
      }
    ]
  }
];

Why this architecture is powerful:

  • Controlled flow: Guards ensure correct progression
  • Preserved state: Shared layout maintains context
  • Optimized UX: Resolvers avoid loading states during navigation

Advanced Pattern: Micro-frontends with Module Federation

Now let's explore an advanced pattern that demonstrates master-level usage.

The Problem with Frontend Monoliths

typescript
// ❌ Limitations of simple approach
const routes: Routes = [
  { path: 'products', loadChildren: () => import('./products/products.module') },
  { path: 'orders', loadChildren: () => import('./orders/orders.module') },
  { path: 'analytics', loadChildren: () => import('./analytics/analytics.module') }
  // All features in same repository = deploy coupling
];

Why this becomes problematic:

  • Different teams can't deploy independently
  • Long builds even for small changes
  • Shared dependencies cause version conflicts

Building the Solution with Module Federation

Stage 1: Shell Application Configuration

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 deep dive:

  • What it does: Allows loading code from separate applications at runtime
  • Why it's powerful: Independent deploy + optimized shared dependencies
  • When to use: Large teams, independent features, frequent releases

Stage 2: Products Micro-frontend

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'])
        }
      }
    ]
  }
];

Integration patterns:

  • Communication via Router: Shared state through query params
  • Event Bus: Custom events for cross-microfrontend communication
  • Shared Services: Injected via dependency injection

Stage 3: Error Handling and 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');
    }
  }
}

// Route with automatic fallback
{
  path: 'products',
  loadChildren: () => inject(MicrofrontendLoaderService).loadMicrofrontend('products'),
  data: { microfrontend: 'products' }
}

Why this architecture is robust:

  • Resilient to failures: Automatic fallbacks when micro-frontends aren't available
  • Independent deploy: Each team can deploy without affecting others
  • Scalability: New micro-frontends can be added without rebuild

Advanced TypeScript Routing

For TypeScript users, here's how to make everything type-safe:

Setting Up Robust Types

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 for 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];
};

Benefits of type safety:

  • Autocomplete: IDE suggests available properties
  • Compile-time checks: Errors detected before runtime

Implementation with Proper Typing

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: 'Product Details',
      preload: true,
      priority: 'high',
      cache: true
    }
  }
];

Advanced TypeScript Patterns

typescript
// guards/typed-guards.ts
import { CanActivateFn, CanMatchFn } from '@angular/router';

// Guard factory with types
export function createRoleGuard(roles: string[]): CanActivateFn {
  return () => {
    const authService = inject(AuthService);
    return authService.hasAnyRole(roles);
  };
}

// Composite guard
export function createCompositeGuard(
  guards: CanActivateFn[]
): CanActivateFn {
  return (route, state) => {
    return guards.every(guard => guard(route, state));
  };
}

// Typed usage
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: 'Admin Panel'
    }
  }
];

Advanced Patterns and Best Practices

1. Strategy Pattern for Preloading

What it solves: Different preload behaviors based on context

How it works: Common interface with specific implementations

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 is 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% navigation chance
  }
  
  getDelay(): number {
    return 1000;
  }
}

// Composite strategy implementation
@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()));
  }
}

When to use: Applications with complex context-based preload requirements

2. Router State Management

The problem: State lost during navigation

The solution: Centralized store for navigation state

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);
      // Keep only last 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);
    }
  }
}

// Usage in components
@Component({
  template: `
    <button (click)="goBack()" [disabled]="!canGoBack()">
      Back
    </button>
  `
})
export class NavigationComponent {
  private routerState = inject(RouterStateService);
  
  get canGoBack() {
    return this.routerState.canGoBack();
  }
  
  goBack() {
    this.routerState.goBack();
  }
}

Benefits: Preserved state, intelligent navigation, better UX

3. Route Data Caching

Use case: Avoid unnecessary re-requests for data that doesn't change frequently

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 with cache
export const cachedProductResolver = (route: ActivatedRouteSnapshot) => {
  const cache = inject(RouteCacheService);
  const productService = inject(ProductService);
  const productId = route.params['id'];
  const cacheKey = `product-${productId}`;
  
  // Check cache first
  const cached = cache.get(cacheKey);
  if (cached) {
    return of(cached);
  }
  
  // If no cache, fetch and store
  return productService.getById(productId).pipe(
    tap(product => cache.set(cacheKey, product, 10)) // Cache for 10 minutes
  );
};

Red flags: Too aggressive caching can show stale data

4. Progressive Enhancement

The concept: Application works even with JavaScript disabled

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 to traditional navigation
    }
    
    // Intercept clicks on links and convert to SPA navigation
    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 {
    // Don't enhance external links
    if (link.hostname !== window.location.hostname) {
      return false;
    }
    
    // Don't enhance downloads
    if (link.download) {
      return false;
    }
    
    return true;
  }
}

Trade-offs: Additional complexity vs better accessibility and SEO

Common Pitfalls to Avoid

1. Over-Engineering Lazy Loading

The problem: Excessive lazy loading for small applications

typescript
// ❌ Don't do this - unnecessary lazy loading for small apps
const routes: Routes = [
  {
    path: 'simple-page',
    loadComponent: () => import('./simple-page.component') // 2KB component
  }
];

// ✅ Do this - direct for simple components
const routes: Routes = [
  {
    path: 'simple-page',
    component: SimplePageComponent // Component already loaded
  }
];

Why this matters: Lazy loading overhead can be greater than the benefit for small components

2. Blocking Resolvers

Common mistake: Resolvers that take too long to resolve

Why it happens: Slow requests or unnecessary sequential requests

typescript
// ❌ Avoid this - resolvers that block navigation
export const slowResolver = (route: ActivatedRouteSnapshot) => {
  const service = inject(SlowService);
  
  // Slow request that blocks navigation
  return service.getSlowData(route.params['id']).pipe(
    delay(5000) // 5 seconds of blocking!
  );
};

// ✅ Solution - async loading in component
@Component({
  template: `
    <div *ngIf="loading">Loading...</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;
    });
  }
}

Prevention: Use resolvers only for critical data, loading states for the rest

3. Memory Leaks in Lazy Modules

The trap: Uncanceled subscriptions in lazy modules

typescript
// ❌ Avoid this - subscription that leaks memory
@Component({
  template: `<div>{{ data$ | async }}</div>`
})
export class LeakyComponent implements OnInit {
  data$!: Observable<any>;
  
  ngOnInit() {
    // Subscription that never gets canceled
    this.service.getData().subscribe(data => {
      // Processing that can leak
    });
  }
}

// ✅ Solution - automatic cleanup
@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();
  }
}

Warning signs: Memory usage growing with repeated navigation

When NOT to Use Lazy Loading

Don't use lazy loading when:

  • Very small applications: Less than 5 main routes
  • Features always used together: Dashboard with interdependent widgets
  • Latency critical: Applications where every millisecond counts
typescript
// ❌ Overkill for simple scenarios
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.routes') // Unnecessary overhead
  }
];

// ✅ Simple solution is better
const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent, // Direct loading
    children: [
      { path: 'overview', component: OverviewComponent },
      { path: 'stats', component: StatsComponent }
    ]
  }
];

Decision framework: Use lazy loading when bundle > 1MB or independent features

Routing vs Redux/NgRx

When Routing Shines

Routing is great for:

  • URL-based state: Filters, pagination, navigation
  • Simple navigation: Between pages and features
  • SEO and deep linking: Friendly and shareable URLs

When to Consider Alternatives

Consider state management when you need:

  • Complex cross-component stateNgRx: For complex state that survives navigation
  • Real-time updatesWebSockets + RxJS: For real-time updating data
  • Optimistic updatesApollo Client: For GraphQL applications with intelligent cache

Comparison Matrix

FeatureAngular RouterNgRxContext API
State in URL✅ Excellent❌ Doesn't support❌ Doesn't support
Performance✅ Lazy loading⚠️ Initial overhead✅ Lightweight
Complexity✅ Simple❌ Learning curve✅ Simple
Debug✅ URL debugging✅ DevTools⚠️ Limited
Time travel❌ Doesn't support✅ Excellent❌ Doesn't support

Conclusion

Routing in Angular 17+ is a powerful tool that can transform slow applications into ultra-fast experiences. It brings intelligent lazy loading, strategic preload, and scalable architectures to your SPAs.

Key takeaways:

  • Lazy loading is essential: But only for independent features and large applications
  • Strategic preload: Use user behavior data to optimize loading
  • Functional guards: Cleaner and more testable than traditional classes
  • TypeScript all the way: Type safety prevents runtime bugs and improves DX

Next time you build an Angular application, remember these strategies. Your users will notice the difference in speed, and your team will thank you for the clean architecture.

Next steps:

  • Analyze your current application and identify lazy loading opportunities
  • Implement an analytics-based preload strategy
  • Set up complete type safety in your routes

Have you already implemented lazy loading in your projects? What patterns have worked best for you? Share your experience in the comments!


If this guide helped you master routing in Angular, follow for more advanced patterns and best practices! 🚀

Resources


Tags do artigo

Artigos relacionados

Receba os ultimos artigos no seu email.

Follow Us: