Skip to content

Angular Lazy Loading Explained — Optimize Bundle Size with Lazy Modules

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Angular Lazy Loading Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Angular lazy loading defers the loading of feature modules and components until they are needed, reducing the initial bundle size and improving application startup time.

What You'll Learn

  • What lazy loading is and why it matters for performance
  • How to lazy load feature modules
  • How to lazy load standalone components
  • How to configure preloading strategies
  • How to analyze bundle sizes

Why It Matters

Lazy loading is essential for large Angular applications. Without it, every feature loads upfront, making the initial page load slow. With lazy loading, users download only what they need and additional features load on demand.

Real-World Use

Durga Antivirus Pro's admin panel loads the main dashboard immediately, while the audit log, user management, and report generation features load lazily. This reduces the initial bundle from 3MB to 400KB.

flowchart TD
    A[App Shell] --> B[Immediately Loaded]
    A --> C[Lazy Feature A]
    A --> D[Lazy Feature B]
    A --> E[Lazy Feature C]
    B --> F[Dashboard]
    C -->|Navigates to /admin/users| G[User Module Loaded]
    D -->|Navigates to /admin/logs| H[Log Module Loaded]
    style A fill:#f97316,color:#fff

Lazy Loading Standalone Components

The simplest approach in modern Angular:

import { Routes } from "@angular/router";

export const routes: Routes = [
  {
    path: "",
    redirectTo: "/home",
    pathMatch: "full"
  },
  {
    path: "home",
    loadComponent: () => import("./home/home.component").then(m => m.HomeComponent)
  },
  {
    path: "users",
    loadComponent: () => import("./user-list/user-list.component").then(m => m.UserListComponent)
  },
  {
    path: "users/:id",
    loadComponent: () => import("./user-detail/user-detail.component").then(m => m.UserDetailComponent)
  },
  {
    path: "settings",
    loadChildren: () => import("./settings/settings.routes").then(m => m.settingsRoutes)
  }
];

Expected output: HomeComponent, UserListComponent, and UserDetailComponent are loaded only when their respective routes are accessed.

loadComponent lazy loads a single standalone component. loadChildren lazy loads a set of child routes. Angular automatically splits the build into separate chunks for each lazy route.

Lazy Loading Feature Modules (Legacy)

For backward compatibility with NgModule-based apps:

// admin.module.ts
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { AdminRoutingModule } from "./admin-routing.module";
import { UserManagementComponent } from "./user-management/user-management.component";
import { AuditLogComponent } from "./audit-log/audit-log.component";

@NgModule({
  declarations: [UserManagementComponent, AuditLogComponent],
  imports: [CommonModule, AdminRoutingModule]
})
export class AdminModule {}

// admin-routing.module.ts
const routes: Routes = [
  { path: "users", component: UserManagementComponent },
  { path: "logs", component: AuditLogComponent },
];

// Main routing module
const routes: Routes = [
  {
    path: "admin",
    loadChildren: () => import("./admin/admin.module").then(m => m.AdminModule)
  }
];

Expected output: The entire AdminModule (both routes) loads lazily when the user navigates to any /admin/* path.

loadChildren with NgModules creates a separate bundle for the feature module. All components declared in the module are included in that lazy chunk.

Preloading Strategies

Preload lazy chunks after the initial load:

import { provideRouter, withPreloading, PreloadingStrategy } from "@angular/router";
import { Injectable } from "@angular/core";
import { Observable, of, timer } from "rxjs";
import { switchMap } from "rxjs/operators";

// Custom preloading strategy
@Injectable({ providedIn: "root" })
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: import("@angular/router").Route, fn: () => Observable<any>): Observable<any> {
    if (route.data?.["preload"]) {
      return fn();
    }
    // Delay preloading by 3 seconds
    return timer(3000).pipe(switchMap(() => fn()));
  }
}

Route configuration with preload hints:

export const routes: Routes = [
  { path: "", component: HomeComponent },
  {
    path: "users",
    loadComponent: () => import("./users/users.component").then(m => m.UsersComponent),
    data: { preload: true } // Preload eagerly after initial load
  },
  {
    path: "reports",
    loadComponent: () => import("./reports/reports.component").then(m => m.ReportsComponent)
    // No data.preload - only load on navigation
  }
];

// App config
provideRouter(routes, withPreloading(CustomPreloadingStrategy))

Expected output: The Users module begins loading 3 seconds after app startup. The Reports module loads only on explicit navigation.

Angular's built-in PreloadAllModules preloads all lazy chunks after the initial render. A custom Strategy gives you fine-grained control over which modules to preload and when.

Analyzing Bundle Sizes

Use the Angular CLI to analyze bundles:

ng build --stats-json
# Then use webpack-bundle-analyzer or esbuild-visualizer on dist/stats.json

Expected output: A treemap visualization showing the size of each lazy chunk, helping you identify large modules that need further splitting.

Regular bundle analysis helps you identify:

  • Components that are larger than expected
  • Shared modules duplicated across lazy chunks
  • Third-party libraries included in critical bundles

Lazy Loading Guards and Resolvers

Guards and resolvers can also be lazy loaded:

export const routes: Routes = [
  {
    path: "users/:id/edit",
    loadComponent: () => import("./edit-user/edit-user.component").then(m => m.EditUserComponent),
    canActivate: [() => import("./guards/auth.guard").then(m => m.authGuard)],
    resolve: {
      user: () => import("./resolvers/user.resolver").then(m => m.userResolver)
    }
  }
];

Expected output: The guard and resolver code are included in the same lazy chunk as the component, not in the main bundle.

Lazy loading guards is critical because guard code may contain references to services and types that should not be loaded until needed.

Common Mistakes

  1. Not splitting shared modules — If multiple lazy modules import the same shared module, it may be duplicated in each chunk. Use shared.module.ts and let the build tool optimize.

  2. Eagerly importing lazy modules — If a lazy module is imported in a non-lazy NgModule's imports array, it loads eagerly. Keep lazy imports only in the router configuration.

  3. Too many small chunks — Creating dozens of tiny lazy chunks increases HTTP requests. Group related features into reasonable-sized lazy modules.

  4. Preloading everythingPreloadAllModules defeats the purpose of lazy loading for low-priority features. Use custom strategies.

  5. Not handling lazy load failures — If a network error prevents a lazy chunk from loading, the navigation fails silently. Add error handling with a splash screen or retry mechanism.

Practice Questions

  1. What is lazy loading in Angular? A technique where modules or components are loaded only when needed, reducing the initial bundle size.

  2. How do you lazy load a standalone component? Use loadComponent: () => import('./path').then(m => m.ComponentName) in the route configuration.

  3. What is the purpose of preloading? To load lazy chunks in the background after the initial render, so navigation to those routes feels instant.

  4. What is loadChildren vs loadComponent? loadChildren lazy loads a module or child routes. loadComponent lazy loads a single standalone component.

  5. How do you prevent duplicate code in lazy chunks? Ensure shared dependencies are marked as shared. Angular's build optimizer deduplicates code across chunks.

Challenge

Build a FeatureFlagLoaderComponent that lazy loads components based on feature flags. Create a registry mapping feature names to component paths. Use dynamic import() inside a function returned by loadComponent. The challenge is to keep the bundle small while supporting 10+ optional features.

FAQ

Does lazy loading affect SEO?

Lazy loading affects client-side rendering. For SEO-critical pages, use SSR or pre-rendering.

How does Angular split code for lazy loading?

Angular's build tool (Esbuild or Webpack) creates a separate chunk for each loadComponent and loadChildren call.

Can I lazy load services?

Services are typically eagerly loaded. Use providedIn: 'root' for shared services. Feature-specific services are included in the lazy chunk.

What is the bundle size impact of lazy loading?

Each lazy chunk includes the component, its template, styles, and dependencies. Without lazy loading, everything is in one large bundle.

Does lazy loading work with module federation?

Yes, Angular's lazy loading integrates with Module Federation for micro-frontends.

Mini Project

Build an EcommerceApp with lazy loaded features: product listing (eager, critical), product detail (lazy), cart (lazy), checkout (lazy with preload), and admin dashboard (lazy, no preload). Use standalone components with loadComponent for each. Implement a custom preloading strategy that preloads the cart after 5 seconds. Use provideRouter with withDebugTracing to see route loading in the console.

What's Next

Continue with server-side rendering and testing:

Angular SSR, Angular Testing, Angular Standalone

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro