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
- Build the app:
ng build - Check
dist/browser/for separate chunk files - Open DevTools Network tab
- Navigate between routes
- Verify chunks load on-demand
Task 2: Test Preloading
- Open Network tab
- Load home page
- Wait a few seconds
- Check if preload routes were fetched
- Navigate to preloaded route (should be instant)
Task 3: Test Loading Indicator
- Throttle network to "Slow 3G"
- Navigate between routes
- Verify loading spinner appears
- Confirm it disappears when loaded
Task 4: Monitor Performance
- Open Console
- Navigate between routes
- Review navigation timing logs
- 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
- Enable build optimizer:
// angular.json
{
"configurations": {
"production": {
"buildOptimizer": true
}
}
}
- 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