最佳實務
效能

解決 Zone 污染

Zone.js 是 Angular 用來偵測應用程式狀態可能已變更的一種訊號機制。它會擷取非同步操作,例如 setTimeout、網路請求與事件監聽器。Angular 會根據來自 Zone.js 的訊號,排程變更偵測。

在某些情況下,排程的工作或微工作並沒有對資料模型做任何變更,這時執行變更偵測就是不必要的。常見的例子有:

  • requestAnimationFrame、setTimeout 或 setInterval
  • 第三方程式庫排程的工作或微工作

本節涵蓋如何找出這種情況,以及如何讓程式碼在 Angular Zone 之外執行,以避免不必要的變更偵測呼叫。

找出不必要的變更偵測呼叫

你可以用 Angular DevTools 偵測不必要的變更偵測呼叫。它們在分析工具的時間軸中,經常以連續的長條出現,來源是 setTimeout、setInterval、requestAnimationFrame 或事件處理常式。當你的應用程式中呼叫這些 API 的地方有限時,變更偵測的叫用通常是由第三方程式庫造成的。

Angular DevTools 分析工具預覽,顯示 Zone 污染

在上圖中,有一連串由與某個元素相關聯的事件處理常式觸發的變更偵測呼叫。在使用第三方的、非原生 Angular 元件時,這是常見的挑戰,因為它們不會改變 NgZone 的預設行為。

在 NgZone 之外執行工作

在這種情況下,你可以用 NgZone,指示 Angular 避免為某段程式碼所排程的工作呼叫變更偵測。

在 Zone 之外執行

1import { Component, NgZone, OnInit, inject } from '@angular/core';
2
3@Component(...)
4class App implements OnInit {
5  private ngZone = inject(NgZone);
6
7  ngOnInit() {
8    this.ngZone.runOutsideAngular(() => setInterval(pollForUpdates, 500));
9  }
10}

前面的程式碼片段指示 Angular 在 Angular Zone 之外呼叫 setInterval,並在 pollForUpdates 執行後略過變更偵測。

當第三方程式庫的 API 在 Angular Zone 內被叫用時,它們經常觸發不必要的變更偵測週期。這種現象對會設定事件監聽器或發起其他工作(例如計時器、XHR 請求等)的程式庫影響特別明顯。只要在 Angular Zone 之外呼叫程式庫 API,就能避免這些額外的週期:

把圖表初始化移到 Zone 之外

1import { Component, NgZone, OnInit, inject } from '@angular/core';
2import * as Plotly from 'plotly.js-dist-min';
3
4@Component(...)
5class App implements OnInit {
6  private ngZone = inject(NgZone);
7
8  ngOnInit() {
9    this.ngZone.runOutsideAngular(() => {
10      Plotly.newPlot('chart', data);
11    });
12  }
13}

在 runOutsideAngular 內執行 Plotly.newPlot('chart', data);,是告訴框架:在初始化邏輯所排程的工作執行之後,不應該執行變更偵測。

例如,若 Plotly.newPlot('chart', data) 為某個 DOM 元素加上事件監聽器,Angular 在它們的處理常式執行後不會執行變更偵測。

但有時候,你可能需要監聽第三方 API 派送的事件。在這種情況下,重要的是要記住:如果初始化邏輯是在 Angular Zone 之外完成的,那些事件監聽器也會在 Angular Zone 之外執行:

檢查處理常式是否在 Zone 之外呼叫

1import { Component, NgZone, OnInit, output, inject } from '@angular/core';
2import * as Plotly from 'plotly.js-dist-min';
3
4@Component(...)
5class App implements OnInit {
6  private ngZone = inject(NgZone);
7
8  plotlyClick = output<Plotly.PlotMouseEvent>();
9
10  ngOnInit() {
11    this.ngZone.runOutsideAngular(() => {
12      this.createPlotly();
13    });
14  }
15
16  private async createPlotly() {
17    const plotly = await Plotly.newPlot('chart', data);
18
19    plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {
20      // This handler will be called outside of the Angular zone because
21      // the initialization logic is also called outside of the zone. To check
22      // 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}

若你需要把事件派送給父元件並執行特定的檢視更新邏輯,你應該考慮重新進入 Angular Zone,指示框架執行變更偵測,或手動執行變更偵測:

派送事件時重新進入 Angular Zone

1import { Component, NgZone, OnInit, output, inject } from '@angular/core';
2import * as Plotly from 'plotly.js-dist-min';
3
4@Component(...)
5class App implements OnInit {
6  private ngZone = inject(NgZone);
7
8  plotlyClick = output<Plotly.PlotMouseEvent>();
9
10  ngOnInit() {
11    this.ngZone.runOutsideAngular(() => {
12      this.createPlotly();
13    });
14  }
15
16  private async createPlotly() {
17    const plotly = await Plotly.newPlot('chart', data);
18
19    plotly.on('plotly_click', (event: Plotly.PlotMouseEvent) => {
20      this.ngZone.run(() => {
21        this.plotlyClick.emit(event);
22      });
23    });
24  }
25}

在 Angular Zone 之外派送事件的情境也可能發生。重要的是要記住,觸發變更偵測(例如手動觸發)可能會導致在 Angular Zone 之外建立/更新檢視。