Skip to main content
Lazy loading allows you to load Angular modules or components on-demand rather than at application startup. This reduces the initial bundle size and improves load times, especially for large applications.

Why Lazy Loading?

Benefits of lazy loading:
  • Smaller Initial Bundle: Load only what’s needed for the initial view
  • Faster Startup: Reduce application bootstrap time
  • Better Performance: Load features only when users need them
  • Efficient Resources: Minimize memory usage by loading code on-demand
Lazy loading is implemented using dynamic imports (import()) and the loadChildren or loadComponent properties in route configurations. The implementation can be found in packages/router/src/router_config_loader.ts.

Lazy Loading Routes

The modern approach uses loadChildren with dynamic imports:
app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'home',
    component: HomeComponent
  },
  {
    path: 'products',
    loadChildren: () => import('./products/products.routes')
      .then(m => m.PRODUCTS_ROUTES)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
  }
];
products/products.routes.ts
import { Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';

export const PRODUCTS_ROUTES: Routes = [
  {
    path: '',
    component: ProductListComponent
  },
  {
    path: ':id',
    component: ProductDetailComponent
  }
];

Default Exports

You can also use default exports:
products/products.routes.ts
import { Routes } from '@angular/router';

const routes: Routes = [
  // ... route definitions
];

export default routes; // Default export
app.routes.ts
export const routes: Routes = [
  {
    path: 'products',
    // .then() is optional with default exports
    loadChildren: () => import('./products/products.routes')
  }
];

Lazy Loading Components

Standalone Components

Lazy load individual components with loadComponent:
app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'about',
    loadComponent: () => import('./about/about.component')
      .then(m => m.AboutComponent)
  },
  {
    path: 'contact',
    loadComponent: () => import('./contact/contact.component')
      .then(m => m.ContactComponent)
  }
];
about/about.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-about',
  standalone: true,
  imports: [CommonModule],
  template: `
    <h1>About Us</h1>
    <p>This component is lazy loaded!</p>
  `
})
export class AboutComponent {}
The loadComponent property is defined in packages/router/src/models.ts:626 and supports lazy loading standalone components.

Lazy Loading NgModules (Legacy)

For applications still using NgModules:
app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('./products/products.module')
      .then(m => m.ProductsModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}
products/products.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailComponent } from './product-detail/product-detail.component';

const routes: Routes = [
  { path: '', component: ProductListComponent },
  { path: ':id', component: ProductDetailComponent }
];

@NgModule({
  declarations: [ProductListComponent, ProductDetailComponent],
  imports: [RouterModule.forChild(routes)]
})
export class ProductsModule {}
NgModule lazy loading is legacy. For new projects, use standalone components with loadComponent or route arrays with loadChildren.

Guards with Lazy Loading

Using canMatch for Lazy Routes

canMatch guards prevent lazy-loaded modules from loading:
feature-flag.guard.ts
import { inject } from '@angular/core';
import { CanMatchFn } from '@angular/router';
import { FeatureFlagService } from './feature-flag.service';

export const featureFlagGuard: CanMatchFn = (route, segments) => {
  const featureFlags = inject(FeatureFlagService);
  const featureName = route.data?.['feature'];
  
  // Prevent module from loading if feature is disabled
  return featureFlags.isEnabled(featureName);
};
routes.ts
export const routes: Routes = [
  {
    path: 'premium',
    loadChildren: () => import('./premium/premium.routes')
      .then(m => m.PREMIUM_ROUTES),
    canMatch: [featureFlagGuard],
    data: { feature: 'premium-features' }
  }
];

Using canActivate

canActivate guards run after the module loads:
auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { AuthService } from './auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.isLoggedIn()) {
    return true;
  }
  
  return router.createUrlTree(['/login']);
};
routes.ts
export const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.routes')
      .then(m => m.DASHBOARD_ROUTES),
    canActivate: [authGuard] // Module loads, then guard runs
  }
];
Use canMatch when you want to prevent loading. Use canActivate when the module should load but activation depends on a condition.

Preloading Strategies

Preloading loads lazy routes in the background after initial load:

PreloadAllModules

Preload all lazy routes automatically:
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { 
  provideRouter, 
  withPreloading, 
  PreloadAllModules 
} from '@angular/router';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules) // Preload all lazy routes
    )
  ]
});

NoPreloading (Default)

Disable preloading (load only on-demand):
main.ts
import { provideRouter, NoPreloading, withPreloading } from '@angular/router';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      routes,
      withPreloading(NoPreloading) // Explicit no preloading
    )
  ]
});

Custom Preloading Strategy

Create a custom strategy for selective preloading:
custom-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({ providedIn: 'root' })
export class CustomPreloadStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Preload if route has 'preload' data property
    if (route.data?.['preload']) {
      // Optional: delay preloading
      const delay = route.data?.['preloadDelay'] || 0;
      return timer(delay).pipe(
        mergeMap(() => {
          console.log('Preloading:', route.path);
          return load();
        })
      );
    }
    
    return of(null); // Don't preload
  }
}
routes.ts
export const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('./products/products.routes')
      .then(m => m.PRODUCTS_ROUTES),
    data: { preload: true } // Will be preloaded
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES),
    data: { 
      preload: true, 
      preloadDelay: 5000 // Preload after 5 seconds
    }
  },
  {
    path: 'reports',
    loadChildren: () => import('./reports/reports.routes')
      .then(m => m.REPORTS_ROUTES)
    // No preload data - won't be preloaded
  }
];
main.ts
import { CustomPreloadStrategy } from './custom-preload.strategy';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      routes,
      withPreloading(CustomPreloadStrategy)
    )
  ]
});

Network-Aware Preloading

Preload based on network conditions:
network-aware-preload.strategy.ts
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class NetworkAwarePreloadStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    const shouldPreload = route.data?.['preload'];
    
    if (!shouldPreload) {
      return of(null);
    }
    
    // Check network connection
    const connection = (navigator as any).connection;
    
    if (connection) {
      // Don't preload on slow connections
      if (connection.effectiveType === 'slow-2g' || 
          connection.effectiveType === '2g') {
        console.log('Skipping preload on slow connection');
        return of(null);
      }
      
      // Don't preload if data saver is on
      if (connection.saveData) {
        console.log('Skipping preload with data saver enabled');
        return of(null);
      }
    }
    
    console.log('Preloading:', route.path);
    return load();
  }
}

Lazy Loading with Providers

Provide services scoped to lazy-loaded routes:
products/products.routes.ts
import { Routes } from '@angular/router';
import { ProductService } from './product.service';
import { ProductListComponent } from './product-list.component';
import { ProductDetailComponent } from './product-detail.component';

export const PRODUCTS_ROUTES: Routes = [
  {
    path: '',
    providers: [
      ProductService // Scoped to this route and children
    ],
    children: [
      {
        path: '',
        component: ProductListComponent
      },
      {
        path: ':id',
        component: ProductDetailComponent
      }
    ]
  }
];

Performance Monitoring

Monitor lazy loading performance:
app.component.ts
import { Component, inject } from '@angular/core';
import { Router, RouteConfigLoadStart, RouteConfigLoadEnd } from '@angular/router';

@Component({
  selector: 'app-root',
  template: `
    <div *ngIf="loading" class="loading-indicator">
      Loading module...
    </div>
    <router-outlet></router-outlet>
  `
})
export class AppComponent {
  private router = inject(Router);
  loading = false;

  ngOnInit() {
    this.router.events.subscribe(event => {
      if (event instanceof RouteConfigLoadStart) {
        this.loading = true;
        console.log('Loading module:', event.route.path);
      } else if (event instanceof RouteConfigLoadEnd) {
        this.loading = false;
        console.log('Module loaded:', event.route.path);
      }
    });
  }
}

Real-World Example

Comprehensive lazy loading setup:
app.routes.ts
import { Routes } from '@angular/router';
import { authGuard } from './guards/auth.guard';
import { adminGuard } from './guards/admin.guard';

export const routes: Routes = [
  // Eager loaded routes
  {
    path: '',
    loadComponent: () => import('./home/home.component')
      .then(m => m.HomeComponent)
  },
  {
    path: 'login',
    loadComponent: () => import('./auth/login.component')
      .then(m => m.LoginComponent)
  },
  
  // Lazy loaded with preloading
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.routes')
      .then(m => m.DASHBOARD_ROUTES),
    canActivate: [authGuard],
    data: { preload: true }
  },
  
  // Lazy loaded without preloading
  {
    path: 'reports',
    loadChildren: () => import('./reports/reports.routes')
      .then(m => m.REPORTS_ROUTES),
    canActivate: [authGuard],
    data: { preload: false }
  },
  
  // Admin section - delayed preload
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES),
    canActivate: [authGuard, adminGuard],
    data: { 
      preload: true,
      preloadDelay: 3000
    }
  },
  
  // Settings - lazy component
  {
    path: 'settings',
    loadComponent: () => import('./settings/settings.component')
      .then(m => m.SettingsComponent),
    canActivate: [authGuard]
  }
];
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withPreloading } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { CustomPreloadStrategy } from './app/custom-preload.strategy';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(
      routes,
      withPreloading(CustomPreloadStrategy)
    )
  ]
});

Bundle Analysis

Analyze your lazy-loaded bundles:
# Generate stats file
ng build --stats-json

# Analyze with webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/your-app/stats.json

Best Practices

Group by Feature

Organize lazy-loaded modules by feature or functionality for better maintainability.

Use Smart Preloading

Implement custom preloading strategies based on user behavior and network conditions.

Monitor Bundle Sizes

Regularly analyze bundle sizes to identify optimization opportunities.

Prefer canMatch for Lazy Routes

Use canMatch guards to prevent unnecessary module downloads.

Common Patterns

Feature Shell Pattern

Create a shell component for lazy-loaded features:
products/products.routes.ts
import { Routes } from '@angular/router';
import { ProductsShellComponent } from './products-shell.component';

export const PRODUCTS_ROUTES: Routes = [
  {
    path: '',
    component: ProductsShellComponent, // Shell with shared layout
    children: [
      {
        path: '',
        loadComponent: () => import('./product-list/product-list.component')
          .then(m => m.ProductListComponent)
      },
      {
        path: ':id',
        loadComponent: () => import('./product-detail/product-detail.component')
          .then(m => m.ProductDetailComponent)
      }
    ]
  }
];

Conditional Lazy Loading

Load different modules based on conditions:
routes.ts
import { inject } from '@angular/core';
import { Routes } from '@angular/router';
import { UserService } from './user.service';

export const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => {
      const userService = inject(UserService);
      
      // Load different modules based on user role
      if (userService.isAdmin()) {
        return import('./admin-dashboard/admin-dashboard.routes')
          .then(m => m.ADMIN_DASHBOARD_ROUTES);
      } else {
        return import('./user-dashboard/user-dashboard.routes')
          .then(m => m.USER_DASHBOARD_ROUTES);
      }
    }
  }
];

Next Steps

Route Guards

Learn how to protect your lazy-loaded routes with guards

Defining Routes

Master route configuration and parameters