Skip to main content

Angular Lazy Loading

Introduction

Lazy loading is one of the most important optimization techniques in Angular applications. Instead of loading the entire application at once, lazy loading allows you to load specific feature modules only when they're needed - typically when a user navigates to a specific route. This approach significantly reduces the initial bundle size, decreases loading time, and improves the overall performance of your application.

In this tutorial, we'll explore how to implement lazy loading in Angular applications, understand the benefits it provides, and see how it integrates with the Angular routing system.

Why Use Lazy Loading?

Before diving into implementation, let's understand why lazy loading is essential:

  1. Faster Initial Load: Only the necessary code is loaded when the application starts
  2. Better Resource Management: Memory is used more efficiently as components are loaded only when needed
  3. Improved User Experience: Users experience faster startup times and smoother navigation
  4. Better Caching: Smaller, separate bundles are easier for browsers to cache

Prerequisites

To follow this tutorial, you should have:

  • Basic understanding of Angular
  • Knowledge of Angular routing
  • Angular CLI installed
  • An existing Angular application (or create a new one)

Setting Up a Project with Lazy Loading

Let's start by creating a sample project structure that demonstrates lazy loading. We'll create a main application with several feature modules:

bash
# Create a new Angular project
ng new lazy-loading-demo --routing

# Navigate to the project directory
cd lazy-loading-demo

# Generate feature modules with routing
ng generate module customers --routing
ng generate module orders --routing
ng generate module admin --routing

Step 1: Create Components for Each Module

Let's add components to each feature module:

bash
# Create components for each module
ng generate component customers/customer-list
ng generate component orders/order-list
ng generate component admin/admin-dashboard

Step 2: Configure Feature Module Routes

Now let's configure the routing for each feature module. First, let's update the customers-routing.module.ts:

typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CustomerListComponent } from './customer-list/customer-list.component';

const routes: Routes = [
{ path: '', component: CustomerListComponent }
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class CustomersRoutingModule { }

Similarly, update the orders-routing.module.ts:

typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { OrderListComponent } from './order-list/order-list.component';

const routes: Routes = [
{ path: '', component: OrderListComponent }
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class OrdersRoutingModule { }

And finally, update the admin-routing.module.ts:

typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';

const routes: Routes = [
{ path: '', component: AdminDashboardComponent }
];

@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class AdminRoutingModule { }

Step 3: Implement Lazy Loading in Root Routing Module

Now, let's update the main app-routing.module.ts to implement lazy loading:

typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
{ path: '', redirectTo: 'customers', pathMatch: 'full' },
{
path: 'customers',
loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)
},
{
path: 'orders',
loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
},
{ path: '**', redirectTo: 'customers' }
];

@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

The key to lazy loading is the loadChildren property that uses the dynamic import() statement. This tells Angular to create a separate bundle for each feature module and load it only when the corresponding route is accessed.

Step 4: Update the App Component Template

Let's update the app.component.html file to include navigation:

html
<div class="container">
<h1>Lazy Loading Demo</h1>

<nav>
<ul>
<li><a routerLink="/customers">Customers</a></li>
<li><a routerLink="/orders">Orders</a></li>
<li><a routerLink="/admin">Admin</a></li>
</ul>
</nav>

<div class="content">
<router-outlet></router-outlet>
</div>
</div>

How Lazy Loading Works Under the Hood

When you build your application with lazy loading enabled, Angular creates separate JavaScript bundles for each lazy-loaded module. These bundles are loaded asynchronously when a user navigates to the corresponding route.

Here's what happens at runtime:

  1. The user navigates to a route, for example, /customers
  2. Angular checks if the module for this route is already loaded
  3. If not, Angular dynamically loads the corresponding module bundle from the server
  4. The module is initialized, and its components are rendered

This process happens transparently to the user, who only notices that the application loads faster initially and remains responsive throughout navigation.

Implementing Preloading Strategies

While lazy loading improves the initial load time, it can cause a slight delay when navigating to a lazy-loaded route for the first time. Angular provides preloading strategies to mitigate this issue.

PreloadAllModules Strategy

This strategy preloads all lazy-loaded modules in the background after the application has loaded:

typescript
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 { }

Custom Preloading Strategy

You can also create custom preloading strategies to selectively preload specific modules:

typescript
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> {
if (route.data && route.data.preload) {
return load();
}
return of(null);
}
}

Then use it in your routing module:

typescript
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { CustomPreloadingStrategy } from './custom-preloading-strategy';

const routes: Routes = [
{
path: 'customers',
loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule),
data: { preload: true }
},
{
path: 'orders',
loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
},
// ... other routes
];

@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: CustomPreloadingStrategy
})],
exports: [RouterModule]
})
export class AppRoutingModule { }

Real-World Example: E-commerce Application

Let's consider a more realistic e-commerce application structure with lazy loading:

typescript
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule, PreloadAllModules } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AuthGuard } from './auth/auth.guard';

const routes: Routes = [
{ path: '', component: HomeComponent, pathMatch: 'full' },
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
{
path: 'cart',
loadChildren: () => import('./cart/cart.module').then(m => m.CartModule)
},
{
path: 'checkout',
loadChildren: () => import('./checkout/checkout.module').then(m => m.CheckoutModule),
canActivate: [AuthGuard]
},
{
path: 'account',
loadChildren: () => import('./account/account.module').then(m => m.AccountModule),
canActivate: [AuthGuard]
},
{
path: 'auth',
loadChildren: () => import('./auth/auth.module').then(m => m.AuthModule)
}
];

@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule { }

In this example:

  • Basic pages like home are eagerly loaded
  • Product catalog, cart, checkout, user account, and authentication modules are lazy-loaded
  • Authentication guard protects routes that require login
  • All modules are preloaded in the background after the initial load

Monitoring Bundle Sizes

To verify that lazy loading is working correctly, you can use the --stats-json flag when building your application:

bash
ng build --stats-json

This generates a stats.json file that you can analyze using tools like Webpack Bundle Analyzer:

bash
npm install webpack-bundle-analyzer --save-dev
npx webpack-bundle-analyzer dist/lazy-loading-demo/stats.json

This will open a visualization of your bundle sizes in the browser, where you can see the main bundle and all lazy-loaded chunks.

Best Practices for Lazy Loading

  1. Feature Organization: Structure your application around feature modules that can be loaded independently
  2. Shared Module: Create a shared module for components, directives, and pipes used across multiple feature modules
  3. Route Level Code-splitting: Align your module boundaries with your routing structure
  4. Smart Preloading: Use preloading strategies to balance initial load time and subsequent navigation speed
  5. Avoid Circular Dependencies: Ensure your modules don't have circular dependencies, as they can break lazy loading
  6. Monitor Bundle Sizes: Regularly check bundle sizes to ensure your lazy loading configuration is effective

Common Issues and Solutions

1. Module Import Errors

Problem: Error when importing components from lazy-loaded modules.

Solution: Make sure you don't import lazy-loaded modules directly in the AppModule. Communication between modules should happen through services.

2. Shared Dependencies

Problem: Duplicated code across multiple lazy-loaded modules.

Solution: Move shared code to a shared module that is imported by each feature module.

3. Deep Linking Issues

Problem: Deep linking directly to lazy-loaded routes causes errors.

Solution: Make sure your server configuration supports Angular's PathLocationStrategy by redirecting all requests to index.html.

Summary

Lazy loading is a powerful technique in Angular that significantly improves application performance by splitting your code into smaller bundles and loading them on demand. By implementing lazy loading:

  • You reduce the initial load time of your application
  • Your application becomes more responsive
  • You use browser caching more effectively
  • Users experience better performance, especially on mobile devices

To implement lazy loading effectively:

  1. Structure your app around feature modules
  2. Configure routes with the loadChildren property
  3. Consider preloading strategies for better user experience
  4. Monitor your bundle sizes to verify the effectiveness

By following the steps in this tutorial, you should now have a good understanding of how to implement and optimize lazy loading in your Angular applications.

Additional Resources

Exercises

  1. Create a new Angular application with at least three feature modules that use lazy loading
  2. Implement a custom preloading strategy that preloads modules based on user role
  3. Add route guards to your lazy-loaded modules to control access
  4. Use the Webpack Bundle Analyzer to visualize and optimize your bundle sizes
  5. Extend an existing application by refactoring it to use lazy loading and measure the performance improvements


If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)