Lazy Loading in Angular
Lazy loading defers the download of feature code until the user actually navigates to that feature. Instead of shipping one giant JavaScript bundle, Angular splits the app into smaller chunks and fetches them on demand — dramatically improving initial load time.
Angular's router handles all the heavy lifting: you just tell it which routes should be loaded lazily.
Why Lazy Loading Matters
Without Lazy Loading | With Lazy Loading |
|---|---|
One large bundle downloaded upfront | Small initial bundle + chunks on demand |
Slow first paint on large apps | Fast initial load regardless of app size |
Users download code they never use | Only visited features are downloaded |
All routes parsed immediately | Route code parsed only when needed |
Lazy Loading Standalone Components
The modern Angular approach uses loadComponent in the route configuration. Angular automatically creates a separate chunk for the imported component and everything it uniquely depends on.
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
// Eagerly loaded — downloaded with the main bundle
{
path: '',
loadComponent: () =>
import('./home/home.component').then(m => m.HomeComponent),
},
// Lazily loaded — fetched only when user visits /dashboard
{
path: 'dashboard',
loadComponent: () =>
import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent),
},
// Lazily loaded admin section
{
path: 'admin',
loadComponent: () =>
import('./admin/admin.component').then(m => m.AdminComponent),
},
];Lazy Loading Feature Routes (loadChildren)
When a feature has multiple pages you can lazy-load an entire route tree with loadChildren. This is the preferred pattern for feature areas because it keeps all related routes co-located.
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'products',
// loadChildren returns a Routes array (standalone) or NgModule
loadChildren: () =>
import('./products/products.routes').then(m => m.PRODUCT_ROUTES),
},
{
path: 'settings',
loadChildren: () =>
import('./settings/settings.routes').then(m => m.SETTINGS_ROUTES),
},
];// products/products.routes.ts
import { Routes } from '@angular/router';
export const PRODUCT_ROUTES: Routes = [
{
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),
},
{
path: ':id/edit',
loadComponent: () =>
import('./product-edit/product-edit.component')
.then(m => m.ProductEditComponent),
},
];Lazy Loading with NgModules (Legacy Pattern)
In NgModule-based apps, loadChildren returns a module class. This pattern is still common in older codebases.
// app-routing.module.ts (NgModule style)
const routes: Routes = [
{
path: 'products',
loadChildren: () =>
import('./products/products.module')
.then(m => m.ProductsModule),
},
];
// products/products.module.ts
@NgModule({
declarations: [ProductListComponent, ProductDetailComponent],
imports: [
CommonModule,
RouterModule.forChild([
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent },
]),
],
})
export class ProductsModule {}RouterModule.forChild(). With standalone components, you export a plain Routes array — no module class needed.Preloading Strategies
Lazy loading fetches code only when navigating. Preloading is a middle ground: load the app quickly first, then silently fetch lazy chunks in the background while the user is idle.
Angular ships two built-in strategies and lets you write custom ones.
// main.ts — configure preloading
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 chunks after init
),
],
});// Custom preloading strategy — only preload routes flagged with data.preload
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class SelectivePreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
return route.data?.['preload'] === true ? load() : of(null);
}
}
// app.routes.ts — mark specific routes for preloading
export const routes: Routes = [
{
path: 'dashboard',
data: { preload: true }, // will be preloaded
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent),
},
{
path: 'reports',
// no preload flag — fetched only on navigation
loadComponent: () => import('./reports/reports.component')
.then(m => m.ReportsComponent),
},
];
// main.ts — use the custom strategy
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withPreloading(SelectivePreloadingStrategy)),
],
});Lazy Loading with Route Guards
Guards run before the lazy chunk is downloaded, preventing unnecessary network requests for unauthorized routes.
// app.routes.ts
import { authGuard } from './guards/auth.guard';
export const routes: Routes = [
{
path: 'admin',
canMatch: [authGuard], // guard runs BEFORE downloading the chunk
loadChildren: () =>
import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
},
];canMatch (not canActivate) with lazy routes when you want to prevent the chunk from downloading for unauthorised users. canActivate still downloads the module; canMatch skips the route entirely.Inspecting Lazy Chunks
Run a production build to see the generated chunks:
ng build --stats-json
Initial chunk files | Names | Raw size | Estimated transfer size main.js | main | 210.23 kB | 56.12 kB polyfills.js | polyfills | 33.08 kB | 10.65 kB Lazy chunk files | Names | Raw size | Estimated transfer size chunk-ABCD1234.js | dashboard | 18.42 kB | 5.10 kB chunk-EFGH5678.js | products | 34.87 kB | 9.20 kB chunk-IJKL9012.js | admin | 12.11 kB | 3.88 kB
Common Pitfalls
Importing a lazy component eagerly elsewhere will pull it into the main bundle — check your imports carefully
Shared services declared in a lazy module get a separate instance — use providedIn: "root" instead
Avoid wildcard imports (import * as m) in loadComponent/loadChildren — they prevent tree-shaking
Always use the .then(m => m.ClassName) pattern; default exports work too but named exports are more explicit
Large shared libraries (e.g. charting) imported in many lazy chunks will be duplicated — hoist them to a shared chunk with optimization.commonChunk in angular.json
Bundle Analyzer Setup
npm install --save-dev webpack-bundle-analyzer ng build --stats-json npx webpack-bundle-analyzer dist/my-app/browser/stats.json
Summary: Lazy loading splits your Angular app into smaller chunks that are fetched on demand. Use loadComponent for individual pages and loadChildren for feature areas with multiple routes. Add a preloading strategy to silently download chunks in the background after the initial render, giving you the best of both worlds: fast startup and near-instant subsequent navigation.