18. Angular Performance Optimization¶
Learning Objectives¶
By the end of this module, you will be able to:
- Identify and resolve common performance bottlenecks in Angular applications
- Implement advanced change detection optimization strategies
- Use lazy loading and code splitting effectively
- Optimize bundle size and loading performance
- Implement virtual scrolling and pagination for large datasets
- Apply memory leak prevention techniques
- Use performance monitoring and profiling tools
- Implement progressive web app features for better performance
Change Detection Optimization¶
OnPush Change Detection Strategy¶
@Component({
selector: 'app-optimized-component',
template: `
<div class="component">
<h3>{{ title }}</h3>
<div *ngFor="let item of items; trackBy: trackByFn">
{{ item.name }} - {{ item.value }}
</div>
<button (click)="updateItems()">Update</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent implements OnInit {
@Input() title = '';
@Input() items: Item[] = [];
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
// Component initialization
}
trackByFn(index: number, item: Item): number {
return item.id;
}
updateItems(): void {
// Create new array reference for OnPush detection
this.items = [...this.items, { id: Date.now(), name: 'New Item', value: 100 }];
this.cdr.markForCheck(); // Manual change detection trigger
}
}
Immutable Data Patterns¶
// Service using immutable patterns
@Injectable({
providedIn: 'root'
})
export class ImmutableDataService {
private dataSubject = new BehaviorSubject<AppState>({
users: [],
loading: false,
error: null
});
public data$ = this.dataSubject.asObservable();
updateUsers(users: User[]): void {
const currentState = this.dataSubject.value;
// Create new state object (immutable update)
const newState: AppState = {
...currentState,
users: [...users],
loading: false
};
this.dataSubject.next(newState);
}
addUser(user: User): void {
const currentState = this.dataSubject.value;
const newState: AppState = {
...currentState,
users: [...currentState.users, user]
};
this.dataSubject.next(newState);
}
updateUser(updatedUser: User): void {
const currentState = this.dataSubject.value;
const newState: AppState = {
...currentState,
users: currentState.users.map(user =>
user.id === updatedUser.id ? { ...user, ...updatedUser } : user
)
};
this.dataSubject.next(newState);
}
}
interface AppState {
users: User[];
loading: boolean;
error: string | null;
}
Advanced TrackBy Functions¶
@Component({
selector: 'app-advanced-list',
template: `
<div class="list">
<!-- Simple trackBy -->
<div *ngFor="let item of items; trackBy: trackById">
{{ item.name }}
</div>
<!-- Complex trackBy with multiple properties -->
<div *ngFor="let user of users; trackBy: trackUserBy">
{{ user.name }} - {{ user.email }}
</div>
<!-- Nested object trackBy -->
<div *ngFor="let order of orders; trackBy: trackOrderBy">
<div *ngFor="let item of order.items; trackBy: trackOrderItemBy">
{{ item.name }} - {{ item.quantity }}
</div>
</div>
</div>
`
})
export class AdvancedListComponent {
items: Item[] = [];
users: User[] = [];
orders: Order[] = [];
// Simple ID-based tracking
trackById(index: number, item: { id: number }): number {
return item.id;
}
// Complex tracking with multiple properties
trackUserBy(index: number, user: User): string {
return `${user.id}-${user.lastModified}`;
}
// Nested object tracking
trackOrderBy(index: number, order: Order): number {
return order.id;
}
trackOrderItemBy(index: number, item: OrderItem): string {
return `${item.productId}-${item.quantity}`;
}
}
Detaching and Reattaching Change Detection¶
@Component({
selector: 'app-detached-component',
template: `
<div>
<button (click)="startHeavyTask()">Start Heavy Task</button>
<button (click)="stopHeavyTask()">Stop Heavy Task</button>
<div>Counter: {{ counter }}</div>
<div>Heavy computation result: {{ result }}</div>
</div>
`
})
export class DetachedComponent implements OnInit, OnDestroy {
counter = 0;
result = 0;
private interval?: number;
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(): void {
// Detach from change detection for performance
this.cdr.detach();
}
ngOnDestroy(): void {
this.stopHeavyTask();
}
startHeavyTask(): void {
this.interval = window.setInterval(() => {
// Heavy computation
this.result = this.heavyComputation();
this.counter++;
// Manual change detection only when needed
this.cdr.detectChanges();
}, 100);
}
stopHeavyTask(): void {
if (this.interval) {
clearInterval(this.interval);
this.interval = undefined;
}
// Reattach to normal change detection
this.cdr.reattach();
}
private heavyComputation(): number {
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
}
}
Bundle Size Optimization¶
Lazy Loading Modules¶
// app-routing.module.ts
const routes: Routes = [
{
path: 'users',
loadChildren: () => import('./users/users.module').then(m => m.UsersModule)
},
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AdminGuard] // Conditional loading
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
// Enable preloading for better UX
preloadingStrategy: PreloadAllModules
})],
exports: [RouterModule]
})
export class AppRoutingModule {}
Custom Preloading Strategy¶
@Injectable({
providedIn: 'root'
})
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Preload based on custom logic
if (route.data && route.data['preload']) {
console.log(`Preloading: ${route.path}`);
return load();
}
return of(null);
}
}
// Usage in routing
const routes: Routes = [
{
path: 'important',
loadChildren: () => import('./important/important.module').then(m => m.ImportantModule),
data: { preload: true }
},
{
path: 'optional',
loadChildren: () => import('./optional/optional.module').then(m => m.OptionalModule),
data: { preload: false }
}
];
Tree Shaking and Dead Code Elimination¶
// Avoid importing entire libraries
// Bad
import * as _ from 'lodash';
// Good - import only what you need
import { debounce, throttle } from 'lodash-es';
// Or use individual packages
import debounce from 'lodash.debounce';
import throttle from 'lodash.throttle';
// Utility service for commonly used functions
@Injectable({
providedIn: 'root'
})
export class UtilsService {
// Import only necessary parts of moment.js
formatDate(date: Date): string {
return format(date, 'yyyy-MM-dd');
}
// Use native APIs when possible
debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: number;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = window.setTimeout(() => func.apply(this, args), wait);
};
}
}
Bundle Analyzer Configuration¶
// package.json scripts
{
"scripts": {
"build:analyze": "ng build --prod --source-map",
"analyze": "npx webpack-bundle-analyzer dist/app-name/main.*.js",
"build:stats": "ng build --prod --stats-json",
"analyze:stats": "npx webpack-bundle-analyzer dist/app-name/stats.json"
}
}
Lazy Loading and Code Splitting¶
Component-Level Lazy Loading¶
@Component({
selector: 'app-dynamic-loader',
template: `
<div class="container">
<button (click)="loadComponent('chart')">Load Chart</button>
<button (click)="loadComponent('calendar')">Load Calendar</button>
<div class="component-container">
<ng-container #dynamicComponent></ng-container>
</div>
</div>
`
})
export class DynamicLoaderComponent {
@ViewChild('dynamicComponent', { read: ViewContainerRef, static: true })
componentContainer!: ViewContainerRef;
private loadedComponents = new Map<string, ComponentRef<any>>();
async loadComponent(type: string): Promise<void> {
// Check if component is already loaded
if (this.loadedComponents.has(type)) {
return;
}
try {
let componentModule: any;
switch (type) {
case 'chart':
componentModule = await import('./components/chart/chart.component');
break;
case 'calendar':
componentModule = await import('./components/calendar/calendar.component');
break;
default:
console.warn(`Unknown component type: ${type}`);
return;
}
const componentFactory = this.componentFactoryResolver
.resolveComponentFactory(componentModule.default);
const componentRef = this.componentContainer.createComponent(componentFactory);
this.loadedComponents.set(type, componentRef);
} catch (error) {
console.error(`Failed to load component ${type}:`, error);
}
}
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
}
Service-Level Lazy Loading¶
// Lazy loading service pattern
@Injectable({
providedIn: 'root'
})
export class LazyServiceLoader {
private serviceCache = new Map<string, any>();
async loadService<T>(serviceName: string, loader: () => Promise<T>): Promise<T> {
if (this.serviceCache.has(serviceName)) {
return this.serviceCache.get(serviceName);
}
try {
const service = await loader();
this.serviceCache.set(serviceName, service);
return service;
} catch (error) {
console.error(`Failed to load service ${serviceName}:`, error);
throw error;
}
}
}
// Usage example
@Component({
selector: 'app-data-processor',
template: `
<button (click)="processData()" [disabled]="processing">
Process Data
</button>
`
})
export class DataProcessorComponent {
processing = false;
constructor(private lazyLoader: LazyServiceLoader) {}
async processData(): Promise<void> {
this.processing = true;
try {
// Lazy load heavy data processing service
const processor = await this.lazyLoader.loadService(
'dataProcessor',
() => import('./services/heavy-data-processor.service')
.then(m => new m.HeavyDataProcessorService())
);
const result = await processor.processLargeDataset();
console.log('Processing result:', result);
} catch (error) {
console.error('Processing failed:', error);
} finally {
this.processing = false;
}
}
}
Virtual Scrolling and Data Optimization¶
CDK Virtual Scrolling¶
@Component({
selector: 'app-virtual-scroll',
template: `
<div class="virtual-scroll-container">
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
<div class="item-content">
<span>{{ item.name }}</span>
<span>{{ item.description }}</span>
</div>
</div>
</cdk-virtual-scroll-viewport>
</div>
`,
styles: [`
.viewport {
height: 400px;
width: 100%;
}
.item {
height: 50px;
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #ddd;
}
.item-content {
display: flex;
justify-content: space-between;
width: 100%;
}
`]
})
export class VirtualScrollComponent implements OnInit {
items: VirtualScrollItem[] = [];
ngOnInit(): void {
// Generate large dataset
this.items = Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`
}));
}
}
interface VirtualScrollItem {
id: number;
name: string;
description: string;
}
Custom Virtual Scrolling Implementation¶
@Component({
selector: 'app-custom-virtual-scroll',
template: `
<div
class="virtual-container"
#container
(scroll)="onScroll($event)">
<div
class="spacer"
[style.height.px]="totalHeight">
<div
class="visible-items"
[style.transform]="'translateY(' + offsetY + 'px)'">
<div
*ngFor="let item of visibleItems; trackBy: trackByFn"
class="item"
[style.height.px]="itemHeight">
{{ item.name }} - {{ item.value }}
</div>
</div>
</div>
</div>
`,
styles: [`
.virtual-container {
height: 400px;
overflow-y: auto;
}
.spacer {
position: relative;
}
.visible-items {
position: absolute;
top: 0;
width: 100%;
}
.item {
display: flex;
align-items: center;
padding: 8px 16px;
border-bottom: 1px solid #eee;
}
`]
})
export class CustomVirtualScrollComponent implements OnInit, AfterViewInit {
@ViewChild('container', { static: true }) container!: ElementRef<HTMLDivElement>;
allItems: ScrollItem[] = [];
visibleItems: ScrollItem[] = [];
itemHeight = 40;
containerHeight = 400;
visibleCount = 0;
totalHeight = 0;
offsetY = 0;
private startIndex = 0;
private endIndex = 0;
ngOnInit(): void {
// Generate large dataset
this.allItems = Array.from({ length: 50000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.floor(Math.random() * 1000)
}));
this.totalHeight = this.allItems.length * this.itemHeight;
}
ngAfterViewInit(): void {
this.containerHeight = this.container.nativeElement.clientHeight;
this.visibleCount = Math.ceil(this.containerHeight / this.itemHeight) + 2; // Buffer
this.updateVisibleItems();
}
onScroll(event: Event): void {
const scrollTop = (event.target as HTMLDivElement).scrollTop;
this.startIndex = Math.floor(scrollTop / this.itemHeight);
this.endIndex = Math.min(
this.startIndex + this.visibleCount,
this.allItems.length
);
this.offsetY = this.startIndex * this.itemHeight;
this.updateVisibleItems();
}
private updateVisibleItems(): void {
this.visibleItems = this.allItems.slice(this.startIndex, this.endIndex);
}
trackByFn(index: number, item: ScrollItem): number {
return item.id;
}
}
interface ScrollItem {
id: number;
name: string;
value: number;
}
Infinite Scrolling with API Integration¶
@Component({
selector: 'app-infinite-scroll',
template: `
<div class="infinite-scroll-container">
<div
*ngFor="let item of items; trackBy: trackByFn"
class="item">
{{ item.title }}
</div>
<div
*ngIf="loading"
class="loading">
Loading more items...
</div>
<div
#sentinel
class="sentinel">
</div>
</div>
`,
styles: [`
.infinite-scroll-container {
height: 500px;
overflow-y: auto;
}
.item {
padding: 16px;
border-bottom: 1px solid #eee;
}
.loading {
padding: 16px;
text-align: center;
color: #666;
}
.sentinel {
height: 1px;
}
`]
})
export class InfiniteScrollComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild('sentinel', { static: true }) sentinel!: ElementRef;
items: ScrollableItem[] = [];
loading = false;
hasMore = true;
private page = 1;
private observer?: IntersectionObserver;
constructor(private dataService: DataService) {}
ngOnInit(): void {
this.loadItems();
}
ngAfterViewInit(): void {
this.setupIntersectionObserver();
}
ngOnDestroy(): void {
if (this.observer) {
this.observer.disconnect();
}
}
private setupIntersectionObserver(): void {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !this.loading && this.hasMore) {
this.loadItems();
}
});
},
{ threshold: 0.1 }
);
this.observer.observe(this.sentinel.nativeElement);
}
private async loadItems(): Promise<void> {
if (this.loading) return;
this.loading = true;
try {
const response = await this.dataService.getItems(this.page, 20).toPromise();
if (response.items.length === 0) {
this.hasMore = false;
} else {
this.items = [...this.items, ...response.items];
this.page++;
}
} catch (error) {
console.error('Failed to load items:', error);
} finally {
this.loading = false;
}
}
trackByFn(index: number, item: ScrollableItem): number {
return item.id;
}
}
Memory Management¶
Subscription Management¶
@Component({
selector: 'app-subscription-manager',
template: `<div>Component with proper subscription management</div>`
})
export class SubscriptionManagerComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
constructor(
private dataService: DataService,
private userService: UserService
) {}
ngOnInit(): void {
// Method 1: takeUntil pattern
this.dataService.getData()
.pipe(takeUntil(this.destroy$))
.subscribe(data => {
console.log('Data received:', data);
});
// Method 2: Multiple subscriptions with takeUntil
combineLatest([
this.userService.currentUser$,
this.dataService.settings$
])
.pipe(takeUntil(this.destroy$))
.subscribe(([user, settings]) => {
console.log('User and settings:', user, settings);
});
// Method 3: Subscription collection (alternative approach)
this.subscriptions.add(
this.dataService.notifications$
.subscribe(notification => {
console.log('Notification:', notification);
})
);
}
ngOnDestroy(): void {
// Emit to complete all subscriptions
this.destroy$.next();
this.destroy$.complete();
// Alternative: unsubscribe from collection
this.subscriptions.unsubscribe();
}
private subscriptions = new Subscription();
}
Memory Leak Detection and Prevention¶
// Memory leak detection service
@Injectable({
providedIn: 'root'
})
export class MemoryLeakDetectionService {
private componentCounts = new Map<string, number>();
registerComponent(componentName: string): void {
const current = this.componentCounts.get(componentName) || 0;
this.componentCounts.set(componentName, current + 1);
if (current > 100) { // Threshold for potential leak
console.warn(`Potential memory leak detected: ${componentName} has ${current} instances`);
}
}
unregisterComponent(componentName: string): void {
const current = this.componentCounts.get(componentName) || 0;
if (current > 0) {
this.componentCounts.set(componentName, current - 1);
}
}
getComponentCounts(): Map<string, number> {
return new Map(this.componentCounts);
}
}
// Base component with memory leak detection
export abstract class BaseComponent implements OnInit, OnDestroy {
protected destroy$ = new Subject<void>();
constructor(
private memoryDetector: MemoryLeakDetectionService,
private componentName: string
) {}
ngOnInit(): void {
this.memoryDetector.registerComponent(this.componentName);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
this.memoryDetector.unregisterComponent(this.componentName);
}
}
Event Listener Cleanup¶
@Component({
selector: 'app-event-cleanup',
template: `
<div class="container" #container>
<button (click)="addGlobalListener()">Add Global Listener</button>
<button (click)="removeGlobalListener()">Remove Global Listener</button>
</div>
`
})
export class EventCleanupComponent implements OnInit, OnDestroy {
@ViewChild('container', { static: true }) container!: ElementRef;
private globalKeyListener?: () => void;
private resizeListener?: () => void;
private listeners: (() => void)[] = [];
ngOnInit(): void {
this.setupEventListeners();
}
ngOnDestroy(): void {
this.cleanupEventListeners();
}
private setupEventListeners(): void {
// Global key listener
this.globalKeyListener = this.renderer.listen(
'document',
'keydown',
(event: KeyboardEvent) => {
if (event.key === 'Escape') {
console.log('Escape pressed');
}
}
);
// Window resize listener
this.resizeListener = this.renderer.listen(
'window',
'resize',
() => {
console.log('Window resized');
}
);
// Store listeners for cleanup
this.listeners.push(this.globalKeyListener, this.resizeListener);
}
private cleanupEventListeners(): void {
// Remove all registered listeners
this.listeners.forEach(removeListener => removeListener());
this.listeners = [];
}
addGlobalListener(): void {
const listener = this.renderer.listen(
'document',
'click',
(event: MouseEvent) => {
console.log('Global click:', event.target);
}
);
this.listeners.push(listener);
}
removeGlobalListener(): void {
if (this.listeners.length > 0) {
const lastListener = this.listeners.pop();
lastListener?.();
}
}
constructor(private renderer: Renderer2) {}
}
Image and Asset Optimization¶
Lazy Loading Images¶
@Directive({
selector: 'img[appLazyLoad]'
})
export class LazyLoadDirective implements OnInit, OnDestroy {
@Input() src = '';
@Input() placeholder = 'assets/placeholder.jpg';
private observer?: IntersectionObserver;
constructor(private el: ElementRef<HTMLImageElement>) {}
ngOnInit(): void {
this.setupLazyLoading();
}
ngOnDestroy(): void {
if (this.observer) {
this.observer.disconnect();
}
}
private setupLazyLoading(): void {
// Set placeholder initially
this.el.nativeElement.src = this.placeholder;
this.el.nativeElement.classList.add('lazy-loading');
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadImage();
}
});
},
{ threshold: 0.1 }
);
this.observer.observe(this.el.nativeElement);
}
private loadImage(): void {
const img = new Image();
img.onload = () => {
this.el.nativeElement.src = this.src;
this.el.nativeElement.classList.remove('lazy-loading');
this.el.nativeElement.classList.add('lazy-loaded');
if (this.observer) {
this.observer.unobserve(this.el.nativeElement);
}
};
img.onerror = () => {
console.error(`Failed to load image: ${this.src}`);
this.el.nativeElement.classList.add('lazy-error');
};
img.src = this.src;
}
}
// Usage
@Component({
template: `
<div class="image-gallery">
<img
*ngFor="let image of images"
[appLazyLoad]
[src]="image.url"
[alt]="image.alt"
placeholder="assets/loading.gif">
</div>
`,
styles: [`
.lazy-loading {
filter: blur(2px);
transition: filter 0.3s;
}
.lazy-loaded {
filter: none;
}
.lazy-error {
border: 2px solid red;
}
`]
})
export class ImageGalleryComponent {}
Progressive Image Loading¶
@Component({
selector: 'app-progressive-image',
template: `
<div class="progressive-image-container">
<img
#lowRes
[src]="lowResSrc"
[alt]="alt"
class="low-res"
[class.loaded]="lowResLoaded"
(load)="onLowResLoad()">
<img
#highRes
[src]="highResSrc"
[alt]="alt"
class="high-res"
[class.loaded]="highResLoaded"
(load)="onHighResLoad()">
<div *ngIf="!lowResLoaded" class="placeholder">
<div class="spinner"></div>
</div>
</div>
`,
styles: [`
.progressive-image-container {
position: relative;
overflow: hidden;
}
.low-res, .high-res {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.3s ease;
}
.low-res.loaded {
opacity: 1;
filter: blur(2px);
}
.high-res.loaded {
opacity: 1;
filter: none;
}
.placeholder {
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
min-height: 200px;
}
`]
})
export class ProgressiveImageComponent {
@Input() lowResSrc = '';
@Input() highResSrc = '';
@Input() alt = '';
lowResLoaded = false;
highResLoaded = false;
onLowResLoad(): void {
this.lowResLoaded = true;
}
onHighResLoad(): void {
this.highResLoaded = true;
}
}
Performance Monitoring¶
Performance Metrics Service¶
@Injectable({
providedIn: 'root'
})
export class PerformanceMonitoringService {
private metrics = new Map<string, PerformanceMetric>();
startMeasurement(name: string): void {
performance.mark(`${name}-start`);
}
endMeasurement(name: string): number {
performance.mark(`${name}-end`);
performance.measure(name, `${name}-start`, `${name}-end`);
const measure = performance.getEntriesByName(name, 'measure')[0];
const duration = measure.duration;
this.recordMetric(name, duration);
return duration;
}
measureFunction<T>(name: string, fn: () => T): T {
this.startMeasurement(name);
const result = fn();
this.endMeasurement(name);
return result;
}
async measureAsyncFunction<T>(name: string, fn: () => Promise<T>): Promise<T> {
this.startMeasurement(name);
try {
const result = await fn();
this.endMeasurement(name);
return result;
} catch (error) {
this.endMeasurement(name);
throw error;
}
}
private recordMetric(name: string, duration: number): void {
const existing = this.metrics.get(name);
if (existing) {
existing.count++;
existing.totalDuration += duration;
existing.averageDuration = existing.totalDuration / existing.count;
existing.maxDuration = Math.max(existing.maxDuration, duration);
existing.minDuration = Math.min(existing.minDuration, duration);
} else {
this.metrics.set(name, {
name,
count: 1,
totalDuration: duration,
averageDuration: duration,
maxDuration: duration,
minDuration: duration
});
}
}
getMetrics(): PerformanceMetric[] {
return Array.from(this.metrics.values());
}
clearMetrics(): void {
this.metrics.clear();
performance.clearMarks();
performance.clearMeasures();
}
}
interface PerformanceMetric {
name: string;
count: number;
totalDuration: number;
averageDuration: number;
maxDuration: number;
minDuration: number;
}
Performance Monitoring Directive¶
@Directive({
selector: '[appPerformanceMonitor]'
})
export class PerformanceMonitorDirective implements OnInit, OnDestroy {
@Input() monitorName = '';
constructor(
private performanceService: PerformanceMonitoringService,
private el: ElementRef
) {}
ngOnInit(): void {
if (this.monitorName) {
this.performanceService.startMeasurement(`component-${this.monitorName}`);
}
}
ngOnDestroy(): void {
if (this.monitorName) {
const duration = this.performanceService.endMeasurement(`component-${this.monitorName}`);
console.log(`Component ${this.monitorName} lifecycle duration: ${duration}ms`);
}
}
}
Progressive Web App Features¶
Service Worker Implementation¶
// app.module.ts
@NgModule({
imports: [
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: environment.production,
registrationStrategy: 'registerWhenStable:30000'
})
]
})
export class AppModule {}
// PWA Service
@Injectable({
providedIn: 'root'
})
export class PWAService {
private promptEvent: any;
canInstall$ = new BehaviorSubject<boolean>(false);
isOnline$ = new BehaviorSubject<boolean>(navigator.onLine);
constructor(private swUpdate: SwUpdate) {
this.initializeServiceWorker();
this.setupInstallPrompt();
this.setupOnlineStatus();
}
private initializeServiceWorker(): void {
if (this.swUpdate.isEnabled) {
// Check for updates
this.swUpdate.available.subscribe(() => {
if (confirm('New version available. Load new version?')) {
window.location.reload();
}
});
// Check for updates periodically
interval(60000).subscribe(() => {
this.swUpdate.checkForUpdate();
});
}
}
private setupInstallPrompt(): void {
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
this.promptEvent = event;
this.canInstall$.next(true);
});
window.addEventListener('appinstalled', () => {
this.canInstall$.next(false);
this.promptEvent = null;
});
}
private setupOnlineStatus(): void {
window.addEventListener('online', () => {
this.isOnline$.next(true);
});
window.addEventListener('offline', () => {
this.isOnline$.next(false);
});
}
async installApp(): Promise<void> {
if (this.promptEvent) {
this.promptEvent.prompt();
const result = await this.promptEvent.userChoice;
if (result.outcome === 'accepted') {
this.canInstall$.next(false);
}
this.promptEvent = null;
}
}
}
Caching Strategy¶
// Cache service for API responses
@Injectable({
providedIn: 'root'
})
export class CacheService {
private cache = new Map<string, CacheItem>();
private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
get<T>(key: string): T | null {
const item = this.cache.get(key);
if (!item) {
return null;
}
if (Date.now() > item.expiresAt) {
this.cache.delete(key);
return null;
}
return item.data as T;
}
set<T>(key: string, data: T, ttl: number = this.DEFAULT_TTL): void {
const item: CacheItem = {
data,
expiresAt: Date.now() + ttl
};
this.cache.set(key, item);
}
delete(key: string): void {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
has(key: string): boolean {
const item = this.cache.get(key);
return item !== undefined && Date.now() <= item.expiresAt;
}
}
interface CacheItem {
data: any;
expiresAt: number;
}
// HTTP Interceptor with caching
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
constructor(private cacheService: CacheService) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// Only cache GET requests
if (req.method !== 'GET') {
return next.handle(req);
}
// Check if response is in cache
const cachedResponse = this.cacheService.get(req.url);
if (cachedResponse) {
return of(new HttpResponse({
status: 200,
body: cachedResponse
}));
}
// Make request and cache response
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cacheService.set(req.url, event.body, 5 * 60 * 1000);
}
})
);
}
}
Best Practices¶
Performance Checklist¶
// Performance audit service
@Injectable({
providedIn: 'root'
})
export class PerformanceAuditService {
auditComponent(componentRef: ComponentRef<any>): PerformanceAuditResult {
const issues: string[] = [];
const recommendations: string[] = [];
// Check change detection strategy
if (componentRef.location.nativeElement.getAttribute('ng-reflect-change-detection') !== 'OnPush') {
issues.push('Component not using OnPush change detection');
recommendations.push('Consider using OnPush change detection strategy');
}
// Check for trackBy functions in *ngFor
const ngForElements = componentRef.location.nativeElement.querySelectorAll('[ng-reflect-ng-for-of]');
ngForElements.forEach((element: Element) => {
if (!element.getAttribute('ng-reflect-ng-for-track-by')) {
issues.push('ngFor without trackBy function detected');
recommendations.push('Add trackBy function to ngFor directives');
}
});
// Check for async pipe usage
const subscriptions = this.countManualSubscriptions(componentRef);
if (subscriptions > 0) {
recommendations.push(`Consider using async pipe instead of ${subscriptions} manual subscriptions`);
}
return {
componentName: componentRef.componentType.name,
issues,
recommendations,
score: this.calculateScore(issues.length, recommendations.length)
};
}
private countManualSubscriptions(componentRef: ComponentRef<any>): number {
// Implementation to count manual subscriptions
return 0; // Simplified
}
private calculateScore(issueCount: number, recommendationCount: number): number {
const maxScore = 100;
const penalty = (issueCount * 20) + (recommendationCount * 5);
return Math.max(0, maxScore - penalty);
}
}
interface PerformanceAuditResult {
componentName: string;
issues: string[];
recommendations: string[];
score: number;
}
Practical Exercises¶
Exercise 1: Performance Dashboard¶
Create a performance monitoring dashboard that:
- Displays real-time performance metrics
- Shows component lifecycle durations
- Monitors memory usage
- Tracks bundle size changes
Exercise 2: Optimize Existing Application¶
Take an existing Angular application and:
- Implement OnPush change detection
- Add virtual scrolling to large lists
- Implement lazy loading for routes and components
- Add performance monitoring
Exercise 3: Build a High-Performance Data Grid¶
Create a data grid component that:
- Handles 100,000+ rows efficiently
- Supports virtual scrolling
- Implements efficient filtering and sorting
- Maintains 60fps performance
Summary¶
This module covered comprehensive Angular performance optimization techniques:
- Change Detection: OnPush strategy, immutable patterns, manual detection control
- Bundle Optimization: Lazy loading, tree shaking, code splitting
- Data Handling: Virtual scrolling, infinite scrolling, caching strategies
- Memory Management: Subscription cleanup, leak detection, event listener management
- Asset Optimization: Lazy loading images, progressive loading
- Monitoring: Performance metrics, profiling tools
- PWA Features: Service workers, caching, offline capabilities
Next Steps¶
- Explore Angular Universal for server-side rendering
- Learn about Angular Elements and micro-frontends
- Study advanced RxJS operators for performance
- Investigate Web Workers for heavy computations
- Practice with performance testing tools