How to Debug a Slow Angular Application

As a senior full-stack developer with extensive experience in large-scale Angular applications, let me walk you through my systematic approach to debugging performance issues.

The key is to follow a methodical process:

  1. Identify Performance Metrics
  2. Profile the Application
  3. Analyze Bottlenecks
  4. Implement Solutions
  5. Measure Improvements

Here’s my implementation approach:

// 1. Performance Monitoring Service
@Injectable({ providedIn: 'root' })
export class PerformanceMonitoringService {
  private metrics = signal<PerformanceMetric[]>([]);
  private logger = inject(LoggingService);
  
  // Track component performance
  trackComponent(componentName: string) {
    const startTime = performance.now();
    const memoryStart = performance.memory?.usedJSHeapSize;
    
    return {
      end: () => {
        const duration = performance.now() - startTime;
        const memoryUsed = performance.memory?.usedJSHeapSize - memoryStart;
        
        this.logMetrics({
          component: componentName,
          duration,
          memoryUsed,
          timestamp: new Date()
        });
      }
    };
  }
  
  // Track change detection cycles
  trackChangeDetection() {
    return {
      start: () => performance.mark('cd_start'),
      end: () => {
        performance.mark('cd_end');
        performance.measure('cd', 'cd_start', 'cd_end');
        
        const duration = performance.getEntriesByName('cd')[0].duration;
        if (duration > 10) {
          this.logger.warn(
            `Long change detection cycle: ${duration}ms`
          );
        }
      }
    };
  }
  
  private logMetrics(metric: PerformanceMetric) {
    this.metrics.update(m => [...m, metric]);
    
    // Log to monitoring service
    if (metric.duration > 100 || metric.memoryUsed > 1000000) {
      this.logger.warn('Performance issue detected', metric);
    }
  }
}

// 2. Network Performance Monitoring
@Injectable()
export class NetworkMonitorInterceptor implements HttpInterceptor {
  private monitor = inject(PerformanceMonitoringService);
  
  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    const startTime = performance.now();
    
    return next.handle(req).pipe(
      finalize(() => {
        const duration = performance.now() - startTime;
        
        if (duration > 1000) {
          this.monitor.logSlowRequest({
            url: req.url,
            method: req.method,
            duration,
            timestamp: new Date()
          });
        }
      })
    );
  }
}

// 3. Optimized Component Example
@Component({
  selector: 'app-optimized',
  template: `
    @if (data(); as items) {
      @for (item of items; track trackById) {
        <item-component 
          [data]="item"
          @deferLoad
        />
      }
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
  private monitor = inject(PerformanceMonitoringService);
  private destroy$ = inject(DestroyRef);
  
  data = signal<any[]>([]);
  loading = signal(false);
  
  ngOnInit() {
    const tracker = this.monitor.trackComponent(
      'OptimizedComponent'
    );
    
    // Monitor memory usage
    interval(5000).pipe(
      takeUntilDestroyed(this.destroy$)
    ).subscribe(() => {
      this.checkMemoryUsage();
    });
    
    // Load data with performance tracking
    this.loading.set(true);
    this.loadData().pipe(
      finalize(() => {
        this.loading.set(false);
        tracker.end();
      })
    ).subscribe();
  }
  
  private checkMemoryUsage() {
    if (performance.memory) {
      const used = performance.memory.usedJSHeapSize;
      const limit = performance.memory.jsHeapSizeLimit;
      
      if (used > limit * 0.8) {
        this.monitor.logMemoryWarning({
          used,
          limit,
          component: 'OptimizedComponent'
        });
      }
    }
  }
}

// 4. Web Worker for Heavy Computations
// performance.worker.ts
self.addEventListener('message', ({ data }) => {
  const { items, operation } = data;
  
  switch (operation) {
    case 'filter':
      const filtered = filterLargeDataset(items);
      self.postMessage({ filtered });
      break;
      
    case 'sort':
      const sorted = sortLargeDataset(items);
      self.postMessage({ sorted });
      break;
  }
});

// 5. Performance Optimization Service
@Injectable({ providedIn: 'root' })
export class PerformanceOptimizationService {
  private worker: Worker;
  
  constructor() {
    this.worker = new Worker(
      new URL('./performance.worker', import.meta.url)
    );
  }
  
  optimizeOperation<T>(
    items: T[],
    operation: 'filter' | 'sort'
  ): Observable<T[]> {
    return new Observable(observer => {
      const startTime = performance.now();
      
      this.worker.postMessage({ items, operation });
      
      const handler = ({ data }: MessageEvent) => {
        const duration = performance.now() - startTime;
        
        if (duration > 100) {
          console.warn(
            `Long operation detected: ${operation} took ${duration}ms`
          );
        }
        
        observer.next(data[operation === 'filter' ? 'filtered' : 'sorted']);
        observer.complete();
      };
      
      this.worker.addEventListener('message', handler);
      
      return () => {
        this.worker.removeEventListener('message', handler);
      };
    });
  }
}

Real-world optimization example:

// Before optimization
@Component({
  template: `
    <div *ngFor="let item of items">
      {{ heavyComputation(item) }}
    </div>
  `
})

// After optimization
@Component({
  template: `
    <virtual-scroller
      #scroll
      [items]="items()"
      [enableUnequalChildrenSizes]="true"
    >
      <div *ngFor="let item of scroll.viewPortItems; trackBy: trackById">
        {{ computedValues()[item.id] }}
      </div>
    </virtual-scroller>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OptimizedComponent {
  items = signal<Item[]>([]);
  
  // Cache computed values
  computedValues = computed(() => {
    const cache = new Map<string, any>();
    
    for (const item of this.items()) {
      if (!cache.has(item.id)) {
        cache.set(
          item.id,
          this.heavyComputation(item)
        );
      }
    }
    
    return Object.fromEntries(cache);
  });
}

When debugging performance issues, I focus on these key areas:

  1. Change Detection

    • Use OnPush strategy
    • Minimize template expressions
    • Track change detection cycles
  2. Memory Management

    // Implement proper cleanup
    ngOnDestroy() {
      // Clear large data structures
      this.items.set([]);
      // Clear caches
      this.cache.clear();
      // Force garbage collection if needed
      if (window.gc) {
        window.gc();
      }
    }
  3. Network Optimization

    // Implement request caching
    @Injectable()
    export class CachingInterceptor {
      private cache = new Map<string, any>();
      
      intercept(req: HttpRequest<any>, next: HttpHandler) {
        if (req.method !== 'GET') {
          return next.handle(req);
        }
        
        const cached = this.cache.get(req.url);
        if (cached) {
          return of(cached);
        }
        
        return next.handle(req).pipe(
          tap(response => {
            this.cache.set(req.url, response);
          })
        );
      }
    }
  4. Bundle Optimization

    // Implement lazy loading
    const routes = [{
      path: 'admin',
      loadChildren: () => import('./admin/admin.module')
    }];
    
    // Use deferred loading
    @defer (on viewport) {
      <heavy-component />
    }

Key points I emphasize in interviews:

  1. Systematic Approach

    • Always profile before optimizing
    • Use Chrome DevTools effectively
    • Monitor key performance metrics
  2. Common Solutions

    • OnPush change detection
    • Virtual scrolling
    • Lazy loading
    • Web Workers
  3. Monitoring

    • Performance metrics
    • Memory usage
    • Network requests
    • Change detection cycles
  4. Best Practices

    • Clean up resources
    • Optimize templates
    • Implement caching
    • Use proper tracking

This approach has helped me optimize several enterprise-level Angular applications, achieving significant performance improvements in both load times and runtime performance.

Test Your Knowledge

Take a quick quiz to test your understanding of this topic.

Test Your Angular Knowledge

Ready to put your skills to the test? Take our interactive Angular quiz and get instant feedback on your answers.