Angular Lazy Loading
Introduction
When building large-scale Angular applications, one of the most common performance challenges is the initial load time. As your application grows with more components, services, and third-party libraries, the bundle size increases, resulting in longer loading times. This is where lazy loading comes to the rescue.
Lazy loading is a design pattern that allows you to load JavaScript components only when they're needed. Instead of loading the entire application at once, Angular allows you to split your application into multiple bundles and load them on demand. This significantly reduces the initial load time, improving the user experience, especially for users with slower network connections.
In this tutorial, we'll explore how to implement lazy loading in Angular applications and understand its benefits for application performance.
Prerequisites
Before diving into lazy loading, make sure you have:
- Basic understanding of Angular
- Familiarity with Angular routing
- Angular CLI installed
Understanding Angular Modules
In Angular, the application is organized into NgModules, which serve as containers for a cohesive block of code dedicated to an application domain, workflow, or closely related set of capabilities.
Before implementing lazy loading, it's important to understand how modules work in Angular:
// Regular eagerly loaded module
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './product-list.component';
@NgModule({
imports: [CommonModule],
declarations: [ProductListComponent],
exports: [ProductListComponent]
})
export class ProductModule { }
By default, Angular loads all modules when the application starts. This is called eager loading. But with lazy loading, we'll change this behavior to load certain modules only when needed.
Setting Up Lazy Loading
Implementing lazy loading in Angular involves several steps:
1. Organize Your Application into Feature Modules
Divide your application into logical feature modules. Each feature module represents a distinct functionality area of your application.
// src/app/products/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductsRoutingModule } from './products-routing.module';
import { ProductListComponent } from './product-list.component';
import { ProductDetailComponent } from './product-detail.component';
@NgModule({
imports: [
CommonModule,
ProductsRoutingModule
],
declarations: [
ProductListComponent,
ProductDetailComponent
]
})
export class ProductsModule { }
2. Configure Routing for Lazy Loading
The key to lazy loading is in how you set up your routes. Instead of directly importing the module, you use the loadChildren
property with a function that returns a Promise via the dynamic import()
syntax:
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) },
{ path: 'products', loadChildren: () => import('./products/products.module').then(m => m.ProductsModule) },
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Notice that we're not importing the modules directly at the top of the file. Instead, we're using the dynamic import()
function, which returns a Promise. This tells Angular to load the module only when the user navigates to the corresponding route.
3. Configure Feature Module Routes
Each feature module should have its own routing module where you define the routes specific to that feature:
// src/app/products/products-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ProductListComponent } from './product-list.component';
import { ProductDetailComponent } from './product-detail.component';
const routes: Routes = [
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class ProductsRoutingModule { }
Notice the use of RouterModule.forChild()
instead of forRoot()
. This is important because we should only call forRoot()
once in our application, typically in the main routing module.
Verifying Lazy Loading
To ensure your lazy loading configuration is working correctly, you can:
-
Check Network Requests: Open your browser's developer tools and navigate to the Network tab. When you navigate to a lazy-loaded route, you should see a new JavaScript bundle being loaded.
-
Using Angular CLI's Bundle Analyzer: You can analyze your bundle sizes using the following command:
ng build --prod --stats-json && npx webpack-bundle-analyzer dist/your-app-name/stats.json
This will generate a visual representation of your bundle sizes, helping you identify which modules are being lazy-loaded.
Real-World Example: E-commerce Application
Let's consider a real-world example of an e-commerce application with the following feature modules:
- Home Module (default landing page)
- Products Module (product listing and details)
- Cart Module (shopping cart functionality)
- User Module (user profile and settings)
- Admin Module (admin dashboard and management)
Setting Up the Project Structure
src/
├── app/
│ ├── home/
│ │ ├── home.component.ts
│ │ ├── home.module.ts
│ │ └── home-routing.module.ts
│ ├── products/
│ │ ├── product-list.component.ts
│ │ ├── product-detail.component.ts
│ │ ├── products.module.ts
│ │ └── products-routing.module.ts
│ ├── cart/
│ │ ├── cart.component.ts
│ │ ├── cart.module.ts
│ │ └── cart-routing.module.ts
│ ├── user/
│ │ ├── profile.component.ts
│ │ ├── settings.component.ts
│ │ ├── user.module.ts
│ │ └── user-routing.module.ts
│ ├── admin/
│ │ ├── dashboard.component.ts
│ │ ├── admin.module.ts
│ │ └── admin-routing.module.ts
│ ├── shared/
│ │ ├── shared.module.ts
│ │ ├── header.component.ts
│ │ └── footer.component.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ └── app-routing.module.ts
Main Routing Configuration
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) },
{ path: 'products', loadChildren: () => import('./products/products.module').then(m => m.ProductsModule) },
{ path: 'cart', loadChildren: () => import('./cart/cart.module').then(m => m.CartModule) },
{ path: 'user', loadChildren: () => import('./user/user.module').then(m => m.UserModule) },
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
With this configuration, the application will only load the necessary code when the user navigates to specific sections, resulting in faster initial load times and better overall performance.
Advanced Lazy Loading Techniques
1. Preloading Strategies
Angular provides a way to preload lazy-loaded modules in the background after the initial page load. This can improve user experience by anticipating which modules the user might navigate to next:
import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
const routes: Routes = [
// ... routes as before
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
With the PreloadAllModules
strategy, Angular preloads all lazy-loaded modules after the initial page has loaded. This can be helpful for applications where users are likely to navigate through multiple sections.
2. Custom Preloading Strategy
You can also create a custom preloading strategy to selectively preload certain modules:
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Only preload routes with data.preload set to true
return route.data && route.data.preload ? load() : of(null);
}
}
Then update your routes:
const routes: Routes = [
{ path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomeModule) },
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule),
data: { preload: true } // This module will be preloaded
},
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: CustomPreloadingStrategy
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
3. Lazy Loading Components (Angular 9+)
With Angular 9 and the Ivy compiler, you can also lazy-load individual components without routing:
import { Component, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<button (click)="loadComponent()">Load Heavy Component</button>
<ng-container #container></ng-container>
`
})
export class AppComponent {
constructor(
private viewContainerRef: ViewContainerRef,
private cfr: ComponentFactoryResolver
) {}
async loadComponent() {
this.viewContainerRef.clear();
const { HeavyComponent } = await import('./heavy/heavy.component');
const componentFactory = this.cfr.resolveComponentFactory(HeavyComponent);
this.viewContainerRef.createComponent(componentFactory);
}
}
Common Pitfalls to Avoid
-
Importing Lazy-Loaded Modules in AppModule: Never import a lazy-loaded module into your main AppModule or any eagerly loaded module. This defeats the purpose of lazy loading.
-
Circular Dependencies: Be careful not to create circular dependencies between your modules, as this can cause issues with lazy loading.
-
Sharing Services: If services need to be shared between eagerly loaded and lazy-loaded modules, make sure to provide them at the root level or use the
providedIn: 'root'
syntax. -
Large Lazy-Loaded Modules: If your lazy-loaded modules are still too large, consider further breaking them down into smaller modules.
Summary
Lazy loading is a powerful technique in Angular that can significantly improve the performance of your applications, particularly those with large feature sets. By loading modules only when needed, you reduce the initial bundle size, leading to faster application startup times and better user experience.
Key takeaways from this tutorial:
- Lazy loading allows you to load code modules on demand, improving initial load time
- Implementation involves organizing your application into feature modules
- Use the
loadChildren
function in your routing configuration to enable lazy loading - Preloading strategies can further enhance user experience
- With newer Angular versions, you can also lazy-load individual components
By applying these techniques to your Angular applications, you can create more performant, responsive web applications that provide a better experience for your users.
Additional Resources
Exercises
-
Create a simple Angular application with three feature modules: Home, Products, and About. Implement lazy loading for all three modules.
-
Implement a custom preloading strategy that preloads modules only when the user has a fast network connection (hint: use the Navigator API to detect connection speed).
-
Analyze your bundle sizes using webpack-bundle-analyzer and identify opportunities for further optimization.
-
Convert an existing eagerly loaded Angular application to use lazy loading and measure the performance improvement.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)