Lazy loading is one of the most powerful optimization techniques available in Angular applications. It allows developers to load feature modules only when they are needed instead of loading the entire application bundle at once. This significantly improves performance, reduces initial loading time, and enhances the user experience — especially in large, enterprise-level applications.
This post explores the concept, advantages, and implementation of lazy loading in Angular in detail. We’ll also cover how it impacts performance, how to structure your application for modularity, and best practices for efficient usage.
1. Understanding Lazy Loading
In Angular, lazy loading refers to the practice of loading feature modules asynchronously when a user navigates to a specific route that requires them. Instead of downloading all code upfront (as in eager loading), Angular loads only the essential parts of the application initially.
When a user navigates to a lazy-loaded module, Angular fetches that module dynamically from the server.
This improves load performance and optimizes bandwidth usage, especially for applications with many features that are not required during the initial load.
2. How Lazy Loading Works
Angular uses the loadChildren
property in the router configuration to enable lazy loading. Instead of directly importing the module, the module path is provided as a string, which Angular resolves when needed.
Example:
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () =>
import('./dashboard/dashboard.module').then(m => m.DashboardModule)
},
{
path: 'settings',
loadChildren: () =>
import('./settings/settings.module').then(m => m.SettingsModule)
}
];
In this setup:
DashboardModule
andSettingsModule
are lazy-loaded.- They will only be loaded when the user navigates to
/dashboard
or/settings
.
3. Eager Loading vs Lazy Loading
Eager Loading
- Loads all feature modules at the start.
- Increases initial load time.
- Suitable for small applications or core modules used across the app.
Lazy Loading
- Loads modules on demand.
- Reduces initial bundle size.
- Ideal for large, modular applications with multiple independent features.
Example comparison:
// Eager loading
import { DashboardModule } from './dashboard/dashboard.module';
@NgModule({
imports: [BrowserModule, DashboardModule]
})
export class AppModule {}
// Lazy loading
const routes: Routes = [
{ path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) }
];
4. Why Use Lazy Loading?
Lazy loading provides several performance and structural benefits. Let’s look at the major ones in depth.
4.1 Faster Initial Load Time
By loading only the essential parts of your application at startup, Angular minimizes the size of the main bundle. This results in:
- Faster loading of the homepage.
- Quicker interaction readiness for users.
- Lower Time to Interactive (TTI) metrics.
Users accessing your site for the first time won’t have to wait for unnecessary code to download.
4.2 Better Performance in Large Applications
For large-scale Angular applications with multiple modules (e.g., admin, user, settings, reports, etc.), loading everything at once can drastically slow down performance.
Lazy loading ensures that only the current route’s module is loaded, keeping memory usage and processing time optimized.
4.3 Separation of Features into Smaller Chunks
Lazy loading enforces modular architecture by splitting the application into smaller, self-contained modules. Each module:
- Can be developed and tested independently.
- Is responsible for a specific feature or functionality.
- Is only loaded when required.
This structure leads to better code organization and maintainability.
4.4 Efficient Use of Bandwidth
Lazy loading ensures that users download only what they need. This is especially beneficial for:
- Mobile users with limited data plans.
- Users in regions with slow internet connections.
By reducing the total amount of downloaded data upfront, the application becomes more bandwidth-efficient.
4.5 Scalability
Lazy loading allows an app to scale easily. As new features are added, they can be encapsulated into new modules without impacting the main application bundle size.
Each feature can be independently lazy-loaded, ensuring consistent performance even as the application grows.
4.6 Improved User Experience
Users experience faster load times and smoother navigation. Since only necessary modules load per route, transitions feel more responsive, and UI rendering becomes faster.
5. Setting Up Lazy Loading Step-by-Step
Let’s go through how to configure lazy loading in an Angular project.
Step 1: Create a Feature Module
ng generate module dashboard --route dashboard --module app.module
This automatically creates a DashboardModule
and configures lazy loading in the router.
Step 2: Add Routing in the Feature Module
Inside dashboard-routing.module.ts
:
const routes: Routes = [
{ path: '', component: DashboardComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DashboardRoutingModule {}
Step 3: Declare Components in the Feature Module
Inside dashboard.module.ts
:
@NgModule({
declarations: [DashboardComponent],
imports: [CommonModule, DashboardRoutingModule]
})
export class DashboardModule {}
Step 4: Configure Main Routing
In app-routing.module.ts
:
const routes: Routes = [
{
path: 'dashboard',
loadChildren: () =>
import('./dashboard/dashboard.module').then(m => m.DashboardModule)
},
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' }
];
6. Preloading Strategy for Lazy Loading
While lazy loading improves performance, sometimes certain modules are frequently accessed (e.g., dashboard or profile). You can preload them in the background using Angular’s PreloadAllModules
strategy.
import { RouterModule, PreloadAllModules } from '@angular/router';
@NgModule({
imports: [
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
],
exports: [RouterModule]
})
export class AppRoutingModule {}
This approach preloads all lazy modules after the app is initially loaded, improving navigation speed for subsequent routes.
7. Route-Level Lazy Loading Example
You can lazy-load multiple modules independently:
const routes: Routes = [
{ path: 'users', loadChildren: () => import('./users/users.module').then(m => m.UsersModule) },
{ path: 'orders', loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule) },
{ path: 'reports', loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule) }
];
Each module will load separately when its corresponding route is visited.
8. Combining Lazy Loading with Guards
You can use route guards to restrict access to lazy-loaded modules.
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AuthGuard]
}
Here, canLoad
prevents the module from loading until the guard condition is met.
9. Lazy Loading and Shared Modules
When using shared components, pipes, or directives across multiple lazy-loaded modules, it’s best to create a SharedModule to avoid duplication.
@NgModule({
declarations: [CustomPipe, SharedComponent],
exports: [CustomPipe, SharedComponent],
imports: [CommonModule]
})
export class SharedModule {}
Then import it in each feature module that needs those shared resources.
10. Performance Benefits of Lazy Loading
The impact of lazy loading is clearly measurable:
- Reduced initial bundle size.
- Faster first contentful paint (FCP).
- Better time-to-interactive (TTI) metrics.
- Improved Lighthouse performance scores.
- Optimized Core Web Vitals.
Lazy loading ensures your Angular application scales well across devices and network conditions.
11. Code Splitting Behind the Scenes
Under the hood, Angular uses Webpack for module bundling. When you define lazy-loaded routes, Webpack automatically splits your application into multiple chunks.
Each chunk (bundle) is fetched dynamically when required, enabling efficient loading and caching.
12. Best Practices for Lazy Loading
- Lazy-load all feature modules not required at app startup.
- Keep shared modules lightweight to avoid large duplicated bundles.
- Avoid circular dependencies between modules.
- Use route guards with
canLoad
to control access. - Implement preloading strategies for frequently accessed modules.
- Use custom preloading strategies to control what loads in the background.
- Analyze bundle size using Angular CLI’s
ng build --stats-json
and visualization tools.
13. Example: Custom Preloading Strategy
You can define a custom preloading strategy to selectively preload specific modules.
@Injectable({ providedIn: 'root' })
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data && route.data['preload'] ? load() : of(null);
}
}
Usage:
const routes: Routes = [
{ path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), data: { preload: true } },
{ path: 'settings', loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule) }
];
14. Testing Lazy-Loaded Modules
You can verify lazy-loaded routes using Angular’s testing utilities:
it('should lazy load dashboard module', async () => {
await router.navigate(['/dashboard']);
expect(location.path()).toBe('/dashboard');
});
This ensures your lazy-loaded modules load correctly and perform as expected.
Leave a Reply