AngularJSLazy Loading

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.

TS
// 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.

TS
// 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),
  },
];

TS
// 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.

TS
// 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 {}
Note
With NgModules, the routing is configured inside the module using 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.

TS
// 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
    ),
  ],
});

TS
// 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.

TS
// 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),
  },
];
Tip
Use 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:

Bash
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

Warning
If you accidentally import a lazily-loaded component in a parent module or eager component, Angular will include it in the initial bundle and you will lose the performance benefit. Use bundle analysis tools like webpack-bundle-analyzer to catch this.
Bundle Analyzer Setup

Bash
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.