使用 measureUserAgent specificMemory() 監控網頁和#39 的記憶體總用量

瞭解如何在實際工作環境中評估網頁的記憶體用量,以偵測迴歸。

Brendan Kenny
Brendan Kenny
Ulan Degenbaev
Ulan Degenbaev

瀏覽器會自動管理網頁的記憶體。每當網頁建立物件時,瀏覽器都會在「幕後」分配一塊記憶體來儲存物件。由於記憶體是有限的資源,瀏覽器會執行垃圾回收作業,偵測物件何時不再需要,並釋放基礎記憶體區塊。

不過,偵測並非完美無缺,艾倫·圖靈的停機問題證明瞭完美偵測是不可能的任務。因此,瀏覽器會以「物件可存取」的概念,來近似「需要物件」的概念。如果網頁無法透過變數和其他可存取物件的欄位存取物件,瀏覽器就能安全地回收物件。這兩個概念的差異會導致記憶體洩漏,如下例所示。

const object = {a: new Array(1000), b: new Array(2000)};
setInterval(() => console.log(object.a), 1000);

這裡不再需要較大的陣列 b,但瀏覽器不會回收該陣列,因為它仍可透過回呼中的 object.b 存取。因此,較大陣列的記憶體會洩漏。

網路上記憶體流失的情況十分普遍,這項研究就顯示了這點。 忘記取消註冊事件監聽器、意外從 iframe 擷取物件、未關閉工作人員,以及在陣列中累積物件等,都可能導致記憶體外洩。如果網頁發生記憶體洩漏,記憶體用量就會隨著時間增加,使用者會覺得網頁載入緩慢且臃腫。

解決這個問題的第一步是進行測量。開發人員可透過全新的 performance.measureUserAgentSpecificMemory() API 測量網頁在正式環境中的記憶體用量,進而偵測出本機測試中漏掉的記憶體洩漏問題。

performance.measureUserAgentSpecificMemory() 與舊版 performance.memory API 有何不同?

如果您熟悉現有的非標準 performance.memory API,可能會想知道新版 API 與舊版有何不同。主要差異在於舊版 API 會傳回 JavaScript 堆積的大小,而新版 API 則會估算網頁使用的記憶體。當 Chrome 與多個網頁 (或同一網頁的多個執行個體) 共用相同堆積時,這項差異就顯得重要。在這種情況下,舊版 API 的結果可能會任意偏移。由於舊版 API 是以實作專屬的字詞 (例如「堆積」) 定義,因此無法標準化。

此外,新版 API 會在垃圾回收期間執行記憶體測量作業。這樣可減少結果中的雜訊,但可能需要一段時間才能產生結果。請注意,其他瀏覽器可能會決定實作新的 API,而不依賴垃圾收集。

建議用途

網頁的記憶體用量取決於事件、使用者動作和垃圾收集的時間。因此,記憶體測量 API 的用途是匯總正式環境的記憶體用量資料。個別通話的結果較不實用。應用情境範例:

  • 在推出新版網頁時偵測迴歸,以找出新的記憶體洩漏問題。
  • 對新功能執行 A/B 測試,評估記憶體影響並偵測記憶體流失。
  • 將記憶體用量與工作階段持續時間相互比較,確認是否有記憶體流失問題。
  • 將記憶體用量與使用者指標相互比較,瞭解記憶體用量的整體影響。

瀏覽器相容性

Browser Support

  • Chrome: 89.
  • Edge: 89.
  • Firefox: not supported.
  • Safari: not supported.

Source

目前這項 API 僅適用於以 Chromium 為基礎的瀏覽器,例如 Chrome 89 以上版本。API 的結果高度取決於實作方式,因為瀏覽器在記憶體中表示物件的方式不同,估算記憶體用量的方式也不同。如果適當的會計作業成本過高或不可行,瀏覽器可能會排除部分記憶體區域。因此,您無法比較不同瀏覽器的結果。只有比較相同瀏覽器的結果才有意義。

使用performance.measureUserAgentSpecificMemory()

特徵偵測

如果執行環境不符合防止跨源資訊洩漏的安全規定,performance.measureUserAgentSpecificMemory 函式將無法使用,或可能因 SecurityError 而失敗。這項功能依賴跨源隔離,網頁可透過設定 COOP+COEP 標頭啟用這項功能。

您可以在執行階段偵測支援情形:

if (!window.crossOriginIsolated) {
  console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
} else if (!performance.measureUserAgentSpecificMemory) {
  console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
} else {
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
    } else {
      throw error;
    }
  }
  console.log(result);
}

本機測試

Chrome 會在垃圾回收期間執行記憶體測量,這表示 API 不會立即解析結果 promise,而是會等待下一次垃圾回收。

呼叫 API 會在逾時後強制執行垃圾回收作業,目前逾時時間設為 20 秒,但可能更快發生。使用 --enable-blink-features='ForceEagerMeasureMemory' 指令列旗標啟動 Chrome,可將逾時時間縮短為零,方便進行本機偵錯和測試。

範例

建議您使用 API 定義全域記憶體監控器,取樣整個網頁的記憶體用量,並將結果傳送至伺服器進行彙整和分析。最簡單的方法是定期取樣,例如每 M 分鐘取樣一次。不過,這樣會導致資料產生偏差,因為記憶體尖峰用量可能出現在樣本之間。

以下範例說明如何使用 Poisson 過程進行無偏差的記憶體測量,確保樣本在任何時間點發生的機率都相同 (示範來源)。

首先,請定義函式,使用 setTimeout() 和隨機間隔排定下一次記憶體測量。

function scheduleMeasurement() {
  // Check measurement API is available.
  if (!window.crossOriginIsolated) {
    console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
    console.log('See https://web.dev/coop-coep/ to learn more')
    return;
  }
  if (!performance.measureUserAgentSpecificMemory) {
    console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
    return;
  }
  const interval = measurementInterval();
  console.log(`Running next memory measurement in ${Math.round(interval / 1000)} seconds`);
  setTimeout(performMeasurement, interval);
}

measurementInterval() 函式會計算隨機間隔 (以毫秒為單位),確保平均每五分鐘進行一次測量。如要瞭解函式背後的數學原理,請參閱「指數分配」。

function measurementInterval() {
  const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000;
  return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS;
}

最後,非同步 performMeasurement() 函式會叫用 API、記錄結果,並排定下一次評估。

async function performMeasurement() {
  // 1. Invoke performance.measureUserAgentSpecificMemory().
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
      return;
    }
    // Rethrow other errors.
    throw error;
  }
  // 2. Record the result.
  console.log('Memory usage:', result);
  // 3. Schedule the next measurement.
  scheduleMeasurement();
}

最後,開始測量。

// Start measurements.
scheduleMeasurement();

結果可能如下所示:

// Console output:
{
  bytes: 60_100_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: [{
        url: 'https://example.com/',
        scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 20_000_000,
      attribution: [{
          url: 'https://example.com/iframe',
          container: {
            id: 'iframe-id-attribute',
            src: '/iframe',
          },
          scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 100_000,
      attribution: [],
      types: ['DOM']
    },
  ],
}

bytes 欄位會傳回記憶體用量總估計值。這個值高度取決於實作方式,無法跨瀏覽器比較。甚至可能因同一瀏覽器的不同版本而異。這個值包含目前程序中所有 iframe、相關視窗和網頁工作站的 JavaScript 和 DOM 記憶體。

breakdown 清單會提供所用記憶體的詳細資訊。每個項目都會說明部分記憶體,並將其歸因於一組由網址識別的視窗、iframe 和 worker。types 欄位會列出與記憶體相關聯的實作專屬記憶體類型。

請務必以一般方式處理所有清單,且不要根據特定瀏覽器硬式編碼假設。舉例來說,部分瀏覽器可能會傳回空白的 breakdown 或空白的 attribution。其他瀏覽器可能會在 attribution 中傳回多個項目,表示無法區分這些項目中哪個擁有記憶體。

意見回饋

網頁效能社群群組和 Chrome 團隊很想瞭解您對 performance.measureUserAgentSpecificMemory() 的想法和使用體驗。

請說明 API 設計

API 是否有任何異常狀況?還是缺少您需要用來實現想法的屬性?在 performance.measureUserAgentSpecificMemory() GitHub 存放區中提出規格問題,或在現有問題中新增您的想法。

回報導入問題

您是否發現 Chrome 實作方式有錯誤?或者實作方式與規格不同?前往 new.crbug.com 回報錯誤。請務必盡可能提供詳細資料、簡單的錯誤重現操作說明,並將「Components」設為 Blink>PerformanceAPIs

顯示支援

你打算使用「performance.measureUserAgentSpecificMemory()」嗎?您的公開支持有助於 Chrome 團隊優先處理功能,並向其他瀏覽器供應商說明支援這些功能的重要性。傳送推文給 @ChromiumDev, 告訴我們您在何處使用這項功能,以及使用方式。

實用連結

特別銘謝

感謝 Domenic Denicola、Yoav Weiss 和 Mathias Bynens 審查 API 設計,以及 Dominik Inführ、Hannes Payer、原健太郎和 Michael Lippautz 審查 Chrome 中的程式碼。此外,我也要感謝 Per Parker、Philipp Weis、Olga Belomestnykh、Matthew Bolohan 和 Neil Mckay 提供寶貴的使用者意見回饋,大幅改善了 API。