Angular Lazy Loading Explained — Optimize Bundle Size with Lazy Modules
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
Not splitting shared modules — If multiple lazy modules import the same shared module, it may be duplicated in each chunk. Use
shared.module.tsand let the build tool optimize.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.
Too many small chunks — Creating dozens of tiny lazy chunks increases HTTP requests. Group related features into reasonable-sized lazy modules.
Preloading everything —
PreloadAllModulesdefeats the purpose of lazy loading for low-priority features. Use custom strategies.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
What is lazy loading in Angular? A technique where modules or components are loaded only when needed, reducing the initial bundle size.
How do you lazy load a standalone component? Use
loadComponent: () => import('./path').then(m => m.ComponentName)in the route configuration.What is the purpose of preloading? To load lazy chunks in the background after the initial render, so navigation to those routes feels instant.
What is
loadChildrenvsloadComponent?loadChildrenlazy loads a module or child routes.loadComponentlazy loads a single standalone component.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
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