Introduction
Angular's Zone.js monkey-patches async APIs (setTimeout, setInterval, event listeners, XMLHttpRequest) to automatically trigger change detection. When third-party libraries use these APIs frequently, they cause unnecessary change detection cycles that degrade performance:
Angular DevTools shows 60+ change detection cycles per second
from a third-party chart library's internal setIntervalThe result is high CPU usage, sluggish UI, and poor performance on lower-end devices.
Symptoms
- High CPU usage even when the application is idle
- Angular DevTools shows excessive change detection cycles
- UI becomes sluggish and unresponsive
- Performance profiler shows
checkAndUpdateViewdominating execution time - Third-party chart/map/animation library causes constant re-rendering
Common Causes
- Third-party library uses
setIntervalfor polling or animation - WebSocket or Server-Sent Events triggering frequent change detection
- Scroll or mousemove event listeners on the window or document
- Library's internal animation loop using
requestAnimationFrame - Multiple third-party libraries each triggering their own change detection
Step-by-Step Fix
- 1.Run third-party initialization outside Angular's zone:
- 2.```typescript
- 3.import { Component, NgZone, OnDestroy, AfterViewInit } from '@angular/core';
- 4.import * as ThirdPartyLib from 'third-party-lib';
@Component({ selector: 'app-chart', template: '<div id="chart-container"></div>', }) export class ChartComponent implements AfterViewInit, OnDestroy { private chart: any;
constructor(private ngZone: NgZone) {}
ngAfterViewInit() { // Initialize outside Angular's zone - no automatic change detection this.ngZone.runOutsideAngular(() => { this.chart = ThirdPartyLib.createChart({ container: document.getElementById('chart-container'), pollingInterval: 1000, }); }); }
ngOnDestroy() { this.chart?.destroy(); } } ```
- 1.Trigger change detection manually only when needed:
- 2.```typescript
- 3.import { ChangeDetectorRef } from '@angular/core';
constructor(private ngZone: NgZone, private cdr: ChangeDetectorRef) {}
ngAfterViewInit() { this.ngZone.runOutsideAngular(() => { this.chart.onDataUpdate((data) => { // Update component state this.chartData = data;
// Manually trigger change detection this.ngZone.run(() => { this.cdr.detectChanges(); }); }); }); } ```
- 1.Use OnPush change detection to limit automatic cycles:
- 2.```typescript
- 3.@Component({
- 4.selector: 'app-dashboard',
- 5.changeDetection: ChangeDetectionStrategy.OnPush,
- 6.template: '<app-chart [data]="chartData"></app-chart>',
- 7.})
- 8.export class DashboardComponent {
- 9.chartData: any;
- 10.// Only updates when chartData reference changes or async pipe emits
- 11.}
- 12.
` - 13.Debounce frequent events from third-party libraries:
- 14.```typescript
- 15.import { fromEvent } from 'rxjs';
- 16.import { debounceTime, takeUntil } from 'rxjs/operators';
ngAfterViewInit() { this.ngZone.runOutsideAngular(() => { fromEvent(window, 'scroll') .pipe(debounceTime(100), takeUntil(this.destroy$)) .subscribe(() => { this.ngZone.run(() => { this.updateScrollPosition(); }); }); }); } ```
Prevention
- Always wrap third-party library initialization in
ngZone.runOutsideAngular() - Use
OnPushchange detection for components with frequently updating data - Profile your application with Angular DevTools to identify zone pollution sources
- Monitor change detection frequency:
ng.profiler.timeChangeDetection() - Use
EventEmitteror RxJS Subjects to bridge outside-zone events back into Angular - Document which third-party libraries require zone management in your codebase
- Consider replacing heavy third-party libraries with lighter Angular-native alternatives