Zone.js is a signaling mechanism that Angular uses to detect when an application state might have changed. It captures asynchronous operations like setTimeout
, network requests, and event listeners. Angular schedules change detection based on signals from Zone.js.
In some cases scheduled tasks or microtasks don’t make any changes in the data model, which makes running change detection unnecessary. Common examples are:
requestAnimationFrame
,setTimeout
orsetInterval
- Task or microtask scheduling by third-party libraries
This section covers how to identify such conditions, and how to run code outside the Angular zone to avoid unnecessary change detection calls.
Identifying unnecessary change detection calls
You can detect unnecessary change detection calls using Angular DevTools. Often they appear as consecutive bars in the profiler’s timeline with source setTimeout
, setInterval
, requestAnimationFrame
, or an event handler. When you have limited calls within your application of these APIs, the change detection invocation is usually caused by a third-party library.
In the image above, there is a series of change detection calls triggered by event handlers associated with an element. That’s a common challenge when using third-party, non-native Angular components, which do not alter the default behavior of NgZone
.
Run tasks outside NgZone
In such cases, you can instruct Angular to avoid calling change detection for tasks scheduled by a given piece of code using NgZone.
Run outside of the Zone
1import { Component, NgZone, OnInit } from '@angular/core';23@Component(...)4class AppComponent implements OnInit {5 constructor(private ngZone: NgZone) {}6 ngOnInit() {7 this.ngZone.runOutsideAngular(() => setInterval(pollForUpdates), 500);8 }9}
The preceding snippet instructs Angular to call setInterval
outside the Angular Zone and skip running change detection after pollForUpdates
runs.
Third-party libraries commonly trigger unnecessary change detection cycles when their APIs are invoked within the Angular zone. This phenomenon particularly affects libraries that setup event listeners or initiate other tasks (such as timers, XHR requests, etc.). Avoid these extra cycles by calling library APIs outside the Angular zone:
Move the plot initialization outside of the Zone
1import { Component, NgZone, OnInit } from '@angular/core';2import * as Plotly from 'plotly.js-dist-min';34@Component(...)5class AppComponent implements OnInit {67 constructor(private ngZone: NgZone) {}89 ngOnInit() {10 this.ngZone.runOutsideAngular(() => {11 Plotly.newPlot('chart', data);12 });13 }14}
Running Plotly.newPlot('chart', data);
within runOutsideAngular
instructs the framework that it shouldn’t run change detection after the execution of tasks scheduled by the initialization logic.
For example, if Plotly.newPlot('chart', data)
adds event listeners to a DOM element, Angular does not run change detection after the execution of their handlers.
But sometimes, you may need to listen to events dispatched by third-party APIs. In such cases, it's important to remember that those event listeners will also execute outside of the Angular zone if the initialization logic was done there:
Check whether the handler is called outside of the Zone
1import { Component, NgZone, OnInit, output } from '@angular/core';2import * as Plotly from 'plotly.js-dist-min';34@Component(...)5class AppComponent implements OnInit {6 plotlyClick = output<Plotly.PlotMouseEvent>();78 constructor(private ngZone: NgZone) {}910 ngOnInit() {11 this.ngZone.runOutsideAngular(() => {12 this.createPlotly();13 });14 }1516 private async createPlotly() {17 const plotly = await Plotly.newPlot('chart', data);1819 plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {20 // This handler will be called outside of the Angular zone because21 // the initialization logic is also called outside of the zone. To check22 // whether we're in the Angular zone, we can call the following:23 console.log(NgZone.isInAngularZone());24 this.plotlyClick.emit(event);25 });26 }27}
If you need to dispatch events to parent components and execute specific view update logic, you should consider re-entering the Angular zone to instruct the framework to run change detection or run change detection manually:
Re-enter the Angular zone when dispatching event
1import { Component, NgZone, OnInit, output } from '@angular/core';2import * as Plotly from 'plotly.js-dist-min';34@Component(...)5class AppComponent implements OnInit {6 plotlyClick = output<Plotly.PlotMouseEvent>();78 constructor(private ngZone: NgZone) {}910 ngOnInit() {11 this.ngZone.runOutsideAngular(() => {12 this.createPlotly();13 });14 }1516 private async createPlotly() {17 const plotly = await Plotly.newPlot('chart', data);1819 plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {20 this.ngZone.run(() => {21 this.plotlyClick.emit(event);22 });23 });24 }25}
The scenario of dispatching events outside of the Angular zone may also arise. It's important to remember that triggering change detection (for example, manually) may result to the creation/update of views outside of the Angular zone.