Skip to main content

Exercise 3: Lazy Loading with loadComponent

Objective

Master lazy loading techniques in modern Angular using loadComponent for standalone components and route-level code splitting.

Prerequisites

  • Completed Exercise 2: Functional Guards & Resolvers
  • Understanding of JavaScript dynamic imports
  • Basic knowledge of Angular routing

What You'll Build

An application with:

  • Lazy-loaded standalone components
  • Feature-based lazy loading
  • Preloading strategies
  • Loading indicators

Time Estimate: 2-3 hours


Part 1: Basic Lazy Loading

Step 1: Create Feature Components

Generate several components for lazy loading:

ng generate component features/products/product-list --standalone
ng generate component features/products/product-detail --standalone
ng generate component features/about --standalone
ng generate component features/contact --standalone

Step 2: Configure Lazy Routes

Update src/app/app.routes.ts:

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

export const routes: Routes = [
{
path: '',
redirectTo: '/home',
pathMatch: 'full',
},
{
path: 'home',
loadComponent: () =>
import('./components/home/home.component').then(m => m.HomeComponent),
},
{
path: 'about',
loadComponent: () =>
import('./features/about/about.component').then(m => m.AboutComponent),
},
{
path: 'contact',
loadComponent: () =>
import('./features/contact/contact.component').then(
m => m.ContactComponent
),
},
{
path: 'products',
loadComponent: () =>
import('./features/products/product-list/product-list.component').then(
m => m.ProductListComponent
),
},
{
path: 'products/:id',
loadComponent: () =>
import(
'./features/products/product-detail/product-detail.component'
).then(m => m.ProductDetailComponent),
},
];

Step 3: Verify Lazy Loading

Update main.ts to enable router debugging:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, withDebugTracing } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes, withDebugTracing()), // Enable route debugging
],
});

Build and check bundle splitting:

ng build --stats-json

Check the dist/stats.json for separate chunk files.


Part 2: Feature Module Lazy Loading

Step 1: Create Feature Routes

Create src/app/features/products/products.routes.ts:

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

export const PRODUCTS_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./product-list/product-list.component').then(
m => m.ProductListComponent
),
},
{
path: ':id',
loadComponent: () =>
import('./product-detail/product-detail.component').then(
m => m.ProductDetailComponent
),
},
{
path: ':id/edit',
loadComponent: () =>
import('./product-edit/product-edit.component').then(
m => m.ProductEditComponent
),
},
];

Step 2: Load Children Routes

Update app.routes.ts:

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

export const routes: Routes = [
{ path: '', redirectTo: '/home', pathMatch: 'full' },
{
path: 'home',
loadComponent: () =>
import('./components/home/home.component').then(m => m.HomeComponent),
},
{
path: 'products',
loadChildren: () =>
import('./features/products/products.routes').then(
m => m.PRODUCTS_ROUTES
),
},
{
path: 'admin',
loadChildren: () =>
import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
},
];

Part 3: Preloading Strategies

Step 1: Default Preloading

Enable preloading all lazy routes:

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import {
provideRouter,
withPreloading,
PreloadAllModules,
} from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
providers: [provideRouter(routes, withPreloading(PreloadAllModules))],
});

Step 2: Custom Preloading Strategy

Create src/app/strategies/selective-preload.strategy.ts:

import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of, timer } from 'rxjs';
import { switchMap } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class SelectivePreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Check if route has preload data
if (route.data?.['preload']) {
const delay = route.data?.['preloadDelay'] || 0;

console.log(`Preloading: ${route.path} with ${delay}ms delay`);

return timer(delay).pipe(switchMap(() => load()));
}

return of(null);
}
}

Step 3: Apply Custom Strategy

// app.routes.ts
export const routes: Routes = [
{
path: 'products',
data: { preload: true, preloadDelay: 2000 }, // Preload after 2s
loadChildren: () =>
import('./features/products/products.routes').then(
m => m.PRODUCTS_ROUTES
),
},
{
path: 'admin',
data: { preload: false }, // Don't preload
loadChildren: () =>
import('./features/admin/admin.routes').then(m => m.ADMIN_ROUTES),
},
];

// main.ts
import { SelectivePreloadStrategy } from './app/strategies/selective-preload.strategy';

bootstrapApplication(AppComponent, {
providers: [provideRouter(routes, withPreloading(SelectivePreloadStrategy))],
});

Part 4: Loading Indicators

Step 1: Create Loading Service

Create src/app/services/loading.service.ts:

import { Injectable, signal } from '@angular/core';

@Injectable({
providedIn: 'root',
})
export class LoadingService {
private loadingSignal = signal(false);
isLoading = this.loadingSignal.asReadonly();

show() {
this.loadingSignal.set(true);
}

hide() {
this.loadingSignal.set(false);
}
}

Step 2: Create Loading Interceptor

Create src/app/interceptors/loading.interceptor.ts:

import { inject } from '@angular/core';
import {
Router,
NavigationStart,
NavigationEnd,
NavigationCancel,
NavigationError,
} from '@angular/router';
import { LoadingService } from '../services/loading.service';

export function initializeLoadingInterceptor() {
const router = inject(Router);
const loadingService = inject(LoadingService);

router.events.subscribe(event => {
if (event instanceof NavigationStart) {
loadingService.show();
}

if (
event instanceof NavigationEnd ||
event instanceof NavigationCancel ||
event instanceof NavigationError
) {
loadingService.hide();
}
});
}

Step 3: Create Loading Component

ng generate component components/loading-spinner --standalone

Update loading-spinner.component.ts:

import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoadingService } from '../../services/loading.service';

@Component({
selector: 'app-loading-spinner',
standalone: true,
imports: [CommonModule],
template: `
@if (loadingService.isLoading()) {
<div class="loading-overlay">
<div class="spinner"></div>
<p>Loading...</p>
</div>
}
`,
styles: [
`
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
}

.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
}

@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

p {
color: white;
margin-top: 20px;
font-size: 18px;
}
`,
],
})
export class LoadingSpinnerComponent {
loadingService = inject(LoadingService);
}

Step 4: Add to App Component

Update app.component.ts:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { LoadingSpinnerComponent } from './components/loading-spinner/loading-spinner.component';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, LoadingSpinnerComponent],
template: `
<app-loading-spinner />
<router-outlet />
`,
})
export class AppComponent {}

Part 5: Network-Aware Preloading

Step 1: Create Network-Aware Strategy

Create src/app/strategies/network-aware-preload.strategy.ts:

import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
providedIn: 'root',
})
export class NetworkAwarePreloadStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data?.['preload']) {
const connection = (navigator as any).connection;

if (connection) {
// Preload on fast connections only
const effectiveType = connection.effectiveType;

if (effectiveType === '4g' || effectiveType === 'wifi') {
console.log(
`Preloading ${route.path} on ${effectiveType} connection`
);
return load();
} else {
console.log(
`Skipping preload of ${route.path} on ${effectiveType} connection`
);
return of(null);
}
}

// Preload if connection info not available
return load();
}

return of(null);
}
}

Part 6: Performance Monitoring

Step 1: Add Performance Tracking

Create src/app/services/performance.service.ts:

import { Injectable } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

@Injectable({
providedIn: 'root',
})
export class PerformanceService {
private navigationStart: number = 0;

constructor(private router: Router) {
this.trackNavigation();
}

private trackNavigation() {
this.router.events
.pipe(filter(event => event instanceof NavigationStart))
.subscribe(() => {
this.navigationStart = performance.now();
});

this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: any) => {
const duration = performance.now() - this.navigationStart;
console.log(`Navigation to ${event.url} took ${duration.toFixed(2)}ms`);

// Track in analytics
this.logPerformance(event.url, duration);
});
}

private logPerformance(url: string, duration: number) {
// Send to analytics service
console.table({
url,
duration: `${duration.toFixed(2)}ms`,
timestamp: new Date().toISOString(),
});
}
}

Testing Tasks

Task 1: Verify Lazy Loading

  1. Build the app: ng build
  2. Check dist/browser/ for separate chunk files
  3. Open DevTools Network tab
  4. Navigate between routes
  5. Verify chunks load on-demand

Task 2: Test Preloading

  1. Open Network tab
  2. Load home page
  3. Wait a few seconds
  4. Check if preload routes were fetched
  5. Navigate to preloaded route (should be instant)

Task 3: Test Loading Indicator

  1. Throttle network to "Slow 3G"
  2. Navigate between routes
  3. Verify loading spinner appears
  4. Confirm it disappears when loaded

Task 4: Monitor Performance

  1. Open Console
  2. Navigate between routes
  3. Review navigation timing logs
  4. Identify slow routes

Challenges

Challenge 1: Conditional Lazy Loading

Create a route that loads different components based on user role.

Challenge 2: Prefetch on Hover

Implement a directive that prefetches a route when user hovers over a link.

Challenge 3: Quota-Aware Loading

Create a strategy that respects browser storage quota.


Bundle Analysis

Analyze Bundle Size

# Install analyzer
npm install -g webpack-bundle-analyzer

# Build with stats
ng build --stats-json

# Analyze
webpack-bundle-analyzer dist/stats.json

Optimize Bundle Size

  1. Enable build optimizer:
// angular.json
{
"configurations": {
"production": {
"buildOptimizer": true
}
}
}
  1. Use dynamic imports for heavy libraries:
async loadChart() {
const { Chart } = await import('chart.js');
// Use Chart
}

Key Takeaways

loadComponent enables standalone component lazy loading ✅ loadChildren for feature-level lazy loading ✅ Preloading strategies improve perceived performance ✅ Custom strategies can optimize for network conditions ✅ Bundle analysis helps identify optimization opportunities

Next Steps

  • Continue practicing lazy loading patterns
  • Explore advanced routing techniques