Skip to content

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:

  1. Displays real-time performance metrics
  2. Shows component lifecycle durations
  3. Monitors memory usage
  4. Tracks bundle size changes

Exercise 2: Optimize Existing Application

Take an existing Angular application and:

  1. Implement OnPush change detection
  2. Add virtual scrolling to large lists
  3. Implement lazy loading for routes and components
  4. Add performance monitoring

Exercise 3: Build a High-Performance Data Grid

Create a data grid component that:

  1. Handles 100,000+ rows efficiently
  2. Supports virtual scrolling
  3. Implements efficient filtering and sorting
  4. 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

Additional Resources