Skip to main content
Performance Optimization

Performance Optimization Overview

Estimated Time: 3 hours | Difficulty: Advanced | Prerequisites: Change Detection, RxJS
Performance optimization in Angular requires understanding bundle sizes, runtime performance, rendering efficiency, and network optimization. Master these techniques to build lightning-fast applications.
┌─────────────────────────────────────────────────────────────────────────┐
│                 Performance Optimization Layers                          │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌─────────────────────────────────────────────────────────────┐       │
│   │                    Build-Time                                │       │
│   │   • Tree shaking      • Code splitting                      │       │
│   │   • Minification      • Compression                         │       │
│   │   • AOT compilation   • Dead code elimination               │       │
│   └─────────────────────────────────────────────────────────────┘       │
│                              │                                           │
│                              ▼                                           │
│   ┌─────────────────────────────────────────────────────────────┐       │
│   │                    Load-Time                                 │       │
│   │   • Lazy loading      • Preloading strategies               │       │
│   │   • Route-based       • Component-level                      │       │
│   │   • Deferrable views  • Initial bundle size                 │       │
│   └─────────────────────────────────────────────────────────────┘       │
│                              │                                           │
│                              ▼                                           │
│   ┌─────────────────────────────────────────────────────────────┐       │
│   │                    Runtime                                   │       │
│   │   • Change detection  • OnPush strategy                     │       │
│   │   • Signals           • Memory management                   │       │
│   │   • Virtual scrolling • TrackBy functions                   │       │
│   └─────────────────────────────────────────────────────────────┘       │
│                              │                                           │
│                              ▼                                           │
│   ┌─────────────────────────────────────────────────────────────┐       │
│   │                    Rendering                                 │       │
│   │   • DOM operations    • CSS containment                     │       │
│   │   • Layout thrashing  • Paint optimization                  │       │
│   │   • Animation perf    • Web Workers                         │       │
│   └─────────────────────────────────────────────────────────────┘       │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Bundle Size Optimization

Build Analysis

# Generate bundle analysis
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json

# Using source-map-explorer
npm install -g source-map-explorer
ng build --source-map
npx source-map-explorer dist/my-app/browser/*.js

Lazy Loading

// app.routes.ts - Lazy load feature modules
export const routes: Routes = [
  { path: '', component: HomeComponent },
  {
    path: 'products',
    loadChildren: () => import('./products/products.routes')
      .then(m => m.PRODUCTS_ROUTES)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES),
    canMatch: [adminGuard]
  }
];

// Component-level lazy loading
@Component({
  template: `
    @if (showEditor()) {
      <ng-container *ngComponentOutlet="editorComponent()" />
    }
  `
})
export class DocumentComponent {
  showEditor = signal(false);
  editorComponent = signal<Type<any> | null>(null);
  
  async loadEditor() {
    const { RichTextEditorComponent } = await import('./rich-text-editor.component');
    this.editorComponent.set(RichTextEditorComponent);
    this.showEditor.set(true);
  }
}

Deferrable Views (Angular 17+)

@Component({
  template: `
    <!-- Defer loading until user scrolls to viewport -->
    @defer (on viewport) {
      <app-heavy-chart [data]="chartData()" />
    } @placeholder {
      <div class="chart-skeleton"></div>
    } @loading (minimum 500ms) {
      <app-spinner />
    }
    
    <!-- Defer until interaction -->
    @defer (on interaction) {
      <app-comments [postId]="postId()" />
    } @placeholder {
      <button>Load Comments</button>
    }
    
    <!-- Defer on idle -->
    @defer (on idle) {
      <app-recommendations />
    }
    
    <!-- Defer with timer -->
    @defer (on timer(2s)) {
      <app-ads />
    }
    
    <!-- Prefetch strategies -->
    @defer (on viewport; prefetch on idle) {
      <app-product-gallery [images]="images()" />
    } @placeholder {
      <div class="gallery-placeholder"></div>
    }
  `
})
export class ProductPageComponent {
  chartData = input<ChartData>();
  postId = input<string>();
  images = input<string[]>();
}

Runtime Performance

OnPush Change Detection

@Component({
  selector: 'app-product-card',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <article class="product-card">
      <img [src]="product().image" [alt]="product().name" />
      <h3>{{ product().name }}</h3>
      <p class="price">{{ product().price | currency }}</p>
      <button (click)="addToCart.emit(product())">Add to Cart</button>
    </article>
  `
})
export class ProductCardComponent {
  product = input.required<Product>();
  addToCart = output<Product>();
}

Signals for Fine-Grained Reactivity

@Component({
  selector: 'app-dashboard',
  template: `
    <div class="dashboard">
      <!-- Only re-renders when stats() changes -->
      <app-stats-panel [stats]="stats()" />
      
      <!-- Only re-renders when filteredItems() changes -->
      <app-items-list [items]="filteredItems()" />
      
      <!-- Only re-renders when totalPrice() changes -->
      <app-price-summary [total]="totalPrice()" />
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent {
  private itemsService = inject(ItemsService);
  
  // Source signals
  items = this.itemsService.items;
  filter = signal<string>('');
  sortBy = signal<'name' | 'price' | 'date'>('name');
  
  // Computed signals - only recalculate when dependencies change
  filteredItems = computed(() => {
    const items = this.items();
    const filterText = this.filter().toLowerCase();
    
    return items
      .filter(item => item.name.toLowerCase().includes(filterText))
      .sort((a, b) => this.sortFn(a, b, this.sortBy()));
  });
  
  stats = computed(() => ({
    total: this.items().length,
    filtered: this.filteredItems().length,
    avgPrice: this.calculateAverage(this.filteredItems())
  }));
  
  totalPrice = computed(() => 
    this.filteredItems().reduce((sum, item) => sum + item.price, 0)
  );
  
  private sortFn(a: Item, b: Item, key: string): number {
    return a[key] > b[key] ? 1 : -1;
  }
  
  private calculateAverage(items: Item[]): number {
    if (items.length === 0) return 0;
    return items.reduce((sum, i) => sum + i.price, 0) / items.length;
  }
}

TrackBy for Lists

@Component({
  template: `
    <!-- ✅ With trackBy - only updates changed items -->
    @for (item of items(); track item.id) {
      <app-item-card [item]="item" />
    }
    
    <!-- Or with track expression -->
    @for (item of items(); track trackById($index, item)) {
      <app-item-card [item]="item" />
    }
  `
})
export class ItemListComponent {
  items = input.required<Item[]>();
  
  trackById(index: number, item: Item): string {
    return item.id;
  }
}

Virtual Scrolling

// Install CDK
// npm install @angular/cdk

import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';

@Component({
  selector: 'app-virtual-list',
  standalone: true,
  imports: [CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf],
  template: `
    <!-- Fixed size items -->
    <cdk-virtual-scroll-viewport 
      itemSize="50" 
      class="viewport"
    >
      <div 
        *cdkVirtualFor="let item of items(); trackBy: trackById"
        class="item"
      >
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: `
    .viewport {
      height: 500px;
      width: 100%;
    }
    
    .item {
      height: 50px;
      display: flex;
      align-items: center;
      padding: 0 16px;
      border-bottom: 1px solid #eee;
    }
  `
})
export class VirtualListComponent {
  items = input.required<Item[]>();
  
  trackById = (index: number, item: Item) => item.id;
}

// Auto-size virtual scroll (variable heights)
@Component({
  template: `
    <cdk-virtual-scroll-viewport 
      autosize
      class="viewport"
    >
      <div 
        *cdkVirtualFor="let message of messages()"
        class="message"
      >
        <img [src]="message.avatar" class="avatar" />
        <div class="content">
          <strong>{{ message.author }}</strong>
          <p>{{ message.text }}</p>
        </div>
      </div>
    </cdk-virtual-scroll-viewport>
  `
})
export class ChatComponent {
  messages = input.required<Message[]>();
}

Image Optimization

NgOptimizedImage Directive

import { NgOptimizedImage, provideImgixLoader } from '@angular/common';

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideImgixLoader('https://my-imgix-domain.imgix.net'),
    // Or cloudinary
    // provideCloudinaryLoader('https://res.cloudinary.com/my-account'),
    // Or custom loader
    // provideImageLoader((config) => `${config.src}?w=${config.width}`)
  ]
};

@Component({
  imports: [NgOptimizedImage],
  template: `
    <!-- Optimized image with automatic srcset -->
    <img 
      ngSrc="hero-image.jpg"
      width="800"
      height="600"
      priority
      placeholder
    />
    
    <!-- Fill mode for responsive images -->
    <div class="image-container">
      <img 
        ngSrc="product.jpg"
        fill
        sizes="(max-width: 768px) 100vw, 50vw"
        [loaderParams]="{ quality: 80 }"
      />
    </div>
    
    <!-- Lazy loaded (default) -->
    <img 
      ngSrc="below-fold.jpg"
      width="400"
      height="300"
      loading="lazy"
    />
  `,
  styles: `
    .image-container {
      position: relative;
      width: 100%;
      aspect-ratio: 16/9;
    }
  `
})
export class GalleryComponent {}

Memory Management

Subscription Cleanup

@Component({
  template: `...`
})
export class DataComponent {
  private destroyRef = inject(DestroyRef);
  private dataService = inject(DataService);
  
  data = signal<Data[]>([]);
  
  ngOnInit() {
    // Auto-cleanup with takeUntilDestroyed
    this.dataService.getData()
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(data => this.data.set(data));
    
    // Or use effect for automatic cleanup
    effect(() => {
      const subscription = this.dataService.stream$.subscribe(
        item => this.processItem(item)
      );
      
      return () => subscription.unsubscribe();
    });
  }
  
  private processItem(item: DataItem) {
    this.data.update(current => [...current, item]);
  }
}

// Using AsyncPipe (auto-cleanup)
@Component({
  template: `
    @if (data$ | async; as data) {
      @for (item of data; track item.id) {
        <app-item [item]="item" />
      }
    }
  `
})
export class DataListComponent {
  data$ = inject(DataService).getData();
}

Web Workers for Heavy Computation

// app.worker.ts
addEventListener('message', ({ data }) => {
  const result = heavyComputation(data);
  postMessage(result);
});

function heavyComputation(data: number[]): number {
  return data.reduce((sum, val) => sum + Math.pow(val, 2), 0);
}

// data-processor.service.ts
@Injectable({ providedIn: 'root' })
export class DataProcessorService {
  private worker: Worker;
  
  constructor() {
    if (typeof Worker !== 'undefined') {
      this.worker = new Worker(
        new URL('./app.worker', import.meta.url)
      );
    }
  }
  
  processData(data: number[]): Observable<number> {
    return new Observable(subscriber => {
      this.worker.onmessage = ({ data }) => {
        subscriber.next(data);
        subscriber.complete();
      };
      
      this.worker.onerror = error => {
        subscriber.error(error);
      };
      
      this.worker.postMessage(data);
    });
  }
}

// Usage in component
@Component({
  template: `
    <button (click)="calculate()">Calculate</button>
    <p>Result: {{ result() }}</p>
  `
})
export class CalculatorComponent {
  private processor = inject(DataProcessorService);
  result = signal<number | null>(null);
  
  calculate() {
    const largeDataSet = Array.from({ length: 1000000 }, () => Math.random());
    
    this.processor.processData(largeDataSet).subscribe(
      result => this.result.set(result)
    );
  }
}

Network Optimization

HTTP Caching

@Injectable({ providedIn: 'root' })
export class CachingInterceptor implements HttpInterceptor {
  private cache = new Map<string, HttpResponse<any>>();
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (!this.isCacheable(req)) {
      return next.handle(req);
    }
    
    const cachedResponse = this.cache.get(req.urlWithParams);
    if (cachedResponse) {
      return of(cachedResponse.clone());
    }
    
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.cache.set(req.urlWithParams, event.clone());
        }
      })
    );
  }
  
  private isCacheable(req: HttpRequest<any>): boolean {
    return req.method === 'GET' && !req.headers.has('x-no-cache');
  }
}

// Stale-while-revalidate pattern
@Injectable({ providedIn: 'root' })
export class StaleWhileRevalidateInterceptor implements HttpInterceptor {
  private cache = new Map<string, { response: HttpResponse<any>; timestamp: number }>();
  private maxAge = 60000; // 1 minute
  
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const cached = this.cache.get(req.urlWithParams);
    
    if (cached) {
      const isStale = Date.now() - cached.timestamp > this.maxAge;
      
      if (isStale) {
        // Return stale, revalidate in background
        next.handle(req).pipe(
          tap(event => {
            if (event instanceof HttpResponse) {
              this.cache.set(req.urlWithParams, {
                response: event.clone(),
                timestamp: Date.now()
              });
            }
          })
        ).subscribe();
        
        return of(cached.response.clone());
      }
      
      return of(cached.response.clone());
    }
    
    return next.handle(req).pipe(
      tap(event => {
        if (event instanceof HttpResponse) {
          this.cache.set(req.urlWithParams, {
            response: event.clone(),
            timestamp: Date.now()
          });
        }
      })
    );
  }
}

Core Web Vitals

// web-vitals.service.ts
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';

@Injectable({ providedIn: 'root' })
export class WebVitalsService {
  private analytics = inject(AnalyticsService);
  
  trackWebVitals() {
    onCLS(metric => this.reportMetric('CLS', metric));
    onINP(metric => this.reportMetric('INP', metric));
    onLCP(metric => this.reportMetric('LCP', metric));
    onFCP(metric => this.reportMetric('FCP', metric));
    onTTFB(metric => this.reportMetric('TTFB', metric));
  }
  
  private reportMetric(name: string, metric: Metric) {
    this.analytics.track('Web Vital', {
      name,
      value: metric.value,
      rating: metric.rating,
      delta: metric.delta,
      id: metric.id
    });
  }
}

Performance Checklist

Build Time

  • Enable production mode
  • AOT compilation
  • Tree shaking enabled
  • Bundle analysis < 200KB initial

Load Time

  • Lazy load routes
  • Defer heavy components
  • Preload strategies
  • Image optimization

Runtime

  • OnPush everywhere
  • Signals for state
  • TrackBy for lists
  • Virtual scrolling

Network

  • HTTP caching
  • Compression (gzip/brotli)
  • CDN for assets
  • Service Worker caching

Next: Content Projection

Master advanced content projection patterns