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
Using loadChildren (Recommended)
The modern approach uses loadChildren with dynamic imports:
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
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:
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 )
}
];
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:
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:
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 );
};
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:
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' ]);
};
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:
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):
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
}
}
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
}
];
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
}
]
}
];
Monitor lazy loading performance:
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:
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 ]
}
];
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:
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