Routing & Navigation

Lazy Loading in Angular

Learn how to lazy-load Angular routes and components to reduce initial bundle size and improve application performance.

What is Lazy Loading?

Lazy loading is a technique where you load parts of your application on demand instead of loading everything upfront. This results in:

  • Smaller initial bundle — faster first load
  • Better performance — load code only when needed
  • Improved user experience — faster time to interactive

How Lazy Loading Works

Without Lazy Loading:
┌──────────────────────────────┐
│ Initial Bundle (Everything)  │  ← Large download
│ Home + Admin + Products +    │
│ Reports + Settings + ...     │
└──────────────────────────────┘

With Lazy Loading:
┌─────────────┐
│ Main Bundle  │  ← Small initial download
│ (Core + Home)│
└─────────────┘
     ↓ User navigates to /admin
┌─────────────┐
│ Admin Chunk  │  ← Loaded on demand
└─────────────┘
     ↓ User navigates to /reports
┌──────────────┐
│ Reports Chunk│  ← Loaded on demand
└──────────────┘

Lazy Loading Routes

Using loadComponent (Standalone)

The simplest way to lazy-load a standalone component:

typescript
// app.routes.ts
export const routes: Routes = [
  { path: '', component: HomeComponent }, // Eagerly loaded

  // Lazy loaded routes
  {
    path: 'products',
    loadComponent: () =>
      import('./products/products.component')
        .then(m => m.ProductsComponent),
  },
  {
    path: 'about',
    loadComponent: () =>
      import('./about/about.component')
        .then(m => m.AboutComponent),
  },
];

Lazy Loading with Child Routes

typescript
// app.routes.ts
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.routes')
        .then(m => m.adminRoutes),
  },
];
typescript
// admin/admin.routes.ts
import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  {
    path: '',
    loadComponent: () =>
      import('./admin-layout.component').then(m => m.AdminLayoutComponent),
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./dashboard/dashboard.component').then(m => m.DashboardComponent),
      },
      {
        path: 'users',
        loadComponent: () =>
          import('./users/users.component').then(m => m.UsersComponent),
      },
      {
        path: 'settings',
        loadComponent: () =>
          import('./settings/settings.component').then(m => m.SettingsComponent),
      },
    ],
  },
];

A Complete Lazy-Loaded Application

typescript
// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { authGuard } from './guards/auth.guard';

export const routes: Routes = [
  // Eagerly loaded (always needed)
  { path: '', component: HomeComponent },

  // Lazy loaded public pages
  {
    path: 'products',
    loadComponent: () =>
      import('./products/product-list.component')
        .then(m => m.ProductListComponent),
  },
  {
    path: 'products/:id',
    loadComponent: () =>
      import('./products/product-detail.component')
        .then(m => m.ProductDetailComponent),
  },
  {
    path: 'contact',
    loadComponent: () =>
      import('./contact/contact.component')
        .then(m => m.ContactComponent),
  },

  // Lazy loaded authenticated area
  {
    path: 'dashboard',
    canActivate: [authGuard],
    loadChildren: () =>
      import('./dashboard/dashboard.routes')
        .then(m => m.dashboardRoutes),
  },

  // Lazy loaded admin area
  {
    path: 'admin',
    canActivate: [authGuard],
    loadChildren: () =>
      import('./admin/admin.routes')
        .then(m => m.adminRoutes),
  },

  // Login
  {
    path: 'login',
    loadComponent: () =>
      import('./auth/login.component')
        .then(m => m.LoginComponent),
  },

  // 404
  {
    path: '**',
    loadComponent: () =>
      import('./not-found/not-found.component')
        .then(m => m.NotFoundComponent),
  },
];

Preloading Strategies

By default, lazy-loaded chunks are loaded only when the route is navigated to. You can preload them in the background:

PreloadAllModules

Load all lazy routes in the background after the initial load:

typescript
// app.config.ts
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withPreloading(PreloadAllModules),
    ),
  ],
};

Custom Preloading Strategy

Only preload routes marked with data.preload:

typescript
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

export class SelectivePreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<unknown>): Observable<unknown> {
    if (route.data?.['preload']) {
      return load(); // Preload this route
    }
    return of(null); // Don't preload
  }
}
typescript
// Route configuration
{
  path: 'products',
  loadComponent: () => import('./products/products.component')
    .then(m => m.ProductsComponent),
  data: { preload: true }, // This route will be preloaded
}
typescript
// Register the strategy
provideRouter(
  routes,
  withPreloading(SelectivePreloadingStrategy),
),

Lazy Loading Components (Non-Route)

You can also lazy-load components that aren't tied to routes using @defer:

@defer Block (Angular 17+)

html
<!-- Load component when it enters the viewport -->
@defer (on viewport) {
  <app-heavy-chart [data]="chartData" />
} @placeholder {
  <div class="chart-placeholder">Chart loading...</div>
} @loading (minimum 500ms) {
  <app-spinner />
} @error {
  <p>Failed to load chart component</p>
}

Defer Triggers

TriggerDescription
on viewportWhen element enters the viewport
on idleWhen browser is idle
on interactionWhen user interacts (click, focus)
on hoverWhen user hovers over placeholder
on immediateImmediately after rendering
on timer(5s)After specified delay
when conditionWhen expression becomes truthy
html
<!-- Load on hover -->
@defer (on hover) {
  <app-tooltip-content />
} @placeholder {
  <span>Hover for details</span>
}

<!-- Load when a condition is met -->
@defer (when showComments) {
  <app-comments [postId]="post.id" />
} @placeholder {
  <button (click)="showComments = true">Load Comments</button>
}

<!-- Load after 3 seconds -->
@defer (on timer(3s)) {
  <app-recommendations />
} @placeholder {
  <div>Loading recommendations...</div>
}

Verifying Lazy Loading

Check your build output to verify chunks are created:

bash
ng build

# Output shows separate chunk files:
Initial chunk files | Names     | Size
main.js            | main      | 150 kB
styles.css         | styles    |  10 kB

Lazy chunk files   | Names     | Size
chunk-ADMIN.js     | admin     |  45 kB
chunk-PRODUCTS.js  | products  |  30 kB
chunk-DASHBOARD.js | dashboard |  25 kB

Best Practices

  1. Lazy load feature areas — admin, settings, reports should always be lazy
  2. Keep the main bundle small — only eagerly load what's needed for the first view
  3. Use @defer for heavy components within a page (charts, editors, maps)
  4. Preload critical routes selectively — use PreloadAllModules or a custom strategy
  5. Group related routes into feature route files for clean lazy loading

Next Steps

With routing and lazy loading covered, it's time to handle user input. Next, we'll explore template-driven forms — the simpler approach to building forms in Angular.