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
ngDoCheckfires 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
// 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
// Add to your component to monitor CD cycles
let cdCount = 0;
ngDoCheck() {
console.log(`Change detection cycle #${++cdCount}`);
}