使用 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 擷取物件、未關閉 worker、在陣列中累積物件等,就很容易引發記憶體外洩。如果網頁有記憶體耗損問題,其記憶體用量會隨著時間增加,對使用者而言,網頁會變得緩慢且龐大。

要解決這個問題,第一步是評估成效。新的 performance.measureUserAgentSpecificMemory() API 可讓開發人員評估正式版網頁的記憶體用量,進而偵測本機測試中遺漏的記憶體外洩情形。

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

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

另一個差異是,新 API 會在垃圾收集期間執行記憶體評估。這樣可以減少結果中的雜訊,但結果可能需要一些時間才會產生。請注意,其他瀏覽器可能會決定實作新的 API,而不依賴垃圾收集。

建議用途

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

  • 在新版本網頁推出期間進行迴歸偵測,找出新的記憶體流失問題。
  • 對新功能進行 A/B 測試,評估其對記憶體的影響並偵測記憶體流失。
  • 將記憶體用量與工作階段時間長度做連結,以驗證是否有記憶體流失。
  • 將記憶體用量與使用者指標做關聯,瞭解記憶體用量對整體的影響。

瀏覽器相容性

瀏覽器支援

  • Chrome:89.
  • Edge:89。
  • Firefox:不支援。
  • Safari:不支援。

資料來源

目前只有 Chrome 89 以上版本的 Chromium 瀏覽器支援此 API。由於瀏覽器在記憶體中呈現物件的方式和估算記憶體用量的方式不同,因此 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 不會立即解析結果承諾,而是等待下一次垃圾收集。

呼叫 API 會在一段逾時時間後強制執行垃圾收集作業,目前設定為 20 秒,但可能會提早執行。使用 --enable-blink-features='ForceEagerMeasureMemory' 指令列標記啟動 Chrome 可將逾時時間縮短至零,這對於本機偵錯和測試很有幫助。

範例

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

以下範例說明如何使用 泊松過程進行無偏差記憶體測量,以確保在任何時間點都同樣可能發生樣本 (demosource)。

首先,請定義一個函式,使用 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 和工作站。types 欄位會列出與記憶體相關聯的特定實作記憶體類型。

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

意見回饋

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

請說明 API 設計

API 是否有任何功能無法正常運作?還是缺少實現創意所需的屬性?請在 performance.measureUserAgentSpecificMemory() GitHub 存放區提交規格問題,或在現有問題中加入您的想法。

回報實作問題

您發現 Chrome 實作錯誤嗎?或者實作方式與規格不同?請前往 new.crbug.com 提交錯誤。請務必盡可能提供詳細資訊,提供重現錯誤的簡單操作說明,並將「元件」設為 Blink>PerformanceAPIsGlitch 可讓您輕鬆快速地分享重現內容。

顯示支援

您是否打算使用 performance.measureUserAgentSpecificMemory()?你的公開支持有助於 Chrome 團隊將功能列為優先,並向其他瀏覽器供應商顯示支援這些功能的重要性。請將 Tweet 傳送給 @ChromiumDev,並告訴我們您使用這項工具的位置和方式。

實用連結

特別銘謝

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

主頁橫幅Harrison BroadbentUnsplash 上提供