The Problem

Angular's Zone.js patches all async APIs (setTimeout, setInterval, addEventListener, etc.) to trigger change detection automatically. Third-party libraries that use these APIs cause Angular to run change detection unnecessarily, degrading performance.

Symptoms

  • Change detection runs hundreds of times per second
  • Angular DevTools shows excessive CD cycles
  • App is sluggish, especially with charts or maps libraries
  • ngDoCheck fires constantly

Real Error Scenario

```typescript import * as Chart from 'chart.js';

@Component({...}) export class DashboardComponent { ngOnInit() { // Chart.js uses setInterval internally for animations // Every interval tick triggers Angular change detection this.chart = new Chart(this.canvas.nativeElement, { type: 'line', data: this.chartData, options: { animation: { duration: 2000 } } }); } } ```

Chart.js's animation loop uses requestAnimationFrame, which Zone.js patches. This triggers change detection ~60 times per second during animations.

How to Fix It

Fix 1: Run Outside Angular Zone

```typescript import { NgZone } from '@angular/core'; import * as Chart from 'chart.js';

@Component({...}) export class DashboardComponent { constructor(private ngZone: NgZone) {}

ngOnInit() { this.ngZone.runOutsideAngular(() => { this.chart = new Chart(this.canvas.nativeElement, { type: 'line', data: this.chartData, options: { animation: { duration: 2000 } } }); }); }

updateChart() { // Re-enter Angular zone to update bindings this.ngZone.run(() => { this.chartData.labels = ['New', 'Labels']; }); } } ```

Fix 2: setInterval Outside Angular

```typescript constructor(private ngZone: NgZone) {}

startPolling() { this.ngZone.runOutsideAngular(() => { this.pollInterval = setInterval(() => { this.fetchData().then(data => { // Only re-enter zone when updating the view this.ngZone.run(() => { this.data = data; }); }); }, 5000); }); }

ngOnDestroy() { clearInterval(this.pollInterval); } ```

Fix 3: Disable Zone.js for Specific APIs

typescript
// zone-flags.ts (import before zone.js in polyfills)
(window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove'];
(window as any).__zone_symbol__DISABLE_WRAPPING_UNCAUGHT_PROMISE_REJECTION = true;

Fix 4: Use Zoneless Change Detection (Angular 17+)

```typescript // app.config.ts import { provideExperimentalZonelessChangeDetection } from '@angular/core';

export const appConfig = { providers: [ provideExperimentalZonelessChangeDetection() ] }; ```

With zoneless mode, you manually trigger change detection only when needed:

```typescript constructor(private cdr: ChangeDetectorRef) {}

updateData() { this.data = newData; this.cdr.markForCheck(); // Manual change detection trigger } ```

Debugging Zone Pollution

typescript
// Add to your component to monitor CD cycles
let cdCount = 0;
ngDoCheck() {
  console.log(`Change detection cycle #${++cdCount}`);
}