Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

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. The key insight is that performance is not one problem — it is four distinct problems that happen at different times in your app’s lifecycle, and the fix for one can actually worsen another if you are not careful. The practical framework: Before optimizing anything, measure first. Use Chrome DevTools Performance tab, Lighthouse, and ng build --stats-json with webpack-bundle-analyzer. Premature optimization based on intuition rather than profiling is the number one waste of engineering time in Angular projects. The 80/20 rule applies heavily: OnPush change detection + lazy loading + proper track in @for loops will solve 80% of performance problems in 20% of the effort.
┌─────────────────────────────────────────────────────────────────────────┐
│                 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+)

Deferrable views are Angular’s answer to the question “how do I lazy-load something that is not a route?” Before @defer, you had to use dynamic imports with NgComponentOutlet or build custom intersection observer logic. Now the framework handles all of that with declarative syntax. The code for the deferred component is not even included in the initial bundle — it is split into a separate chunk automatically.
Practical tip: Use @defer (on viewport; prefetch on idle) for below-the-fold content. This prefetches the chunk during browser idle time, so by the time the user scrolls down, the code is already cached and the component renders instantly. This gives you the bundle size benefit of lazy loading with zero perceived latency.
@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

Memory leaks from unsubscribed observables are the silent killer of Angular app performance. Every subscription that outlives its component is a function reference that prevents garbage collection — and each one adds overhead to every change detection cycle. In production, this manifests as the app gradually slowing down the longer a user keeps a tab open.
@Component({
  template: `...`
})
export class DataComponent {
  private destroyRef = inject(DestroyRef);
  private dataService = inject(DataService);
  
  data = signal<Data[]>([]);
  
  ngOnInit() {
    // takeUntilDestroyed() automatically completes the observable
    // when the component's DestroyRef fires. This is the modern
    // replacement for the manual Subject + takeUntil + ngOnDestroy pattern.
    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

Client-side HTTP caching can eliminate redundant network requests entirely. The simplest pattern is a Map-based cache for GET requests. For more sophisticated needs, the “stale-while-revalidate” pattern (shown below) gives users instant responses from cache while silently refreshing data in the background — this is the same strategy that Chrome’s HTTP cache uses and what makes apps feel “snappy.”
Pitfall: A naive cache interceptor with no TTL or invalidation will serve stale data forever. Always pair caching with either a max-age, a manual invalidation mechanism, or the stale-while-revalidate pattern. For data that changes frequently (e.g., stock prices, notifications), do not cache at the HTTP level — use the service worker’s freshness strategy instead.
@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