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

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

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 的差異。主要差異在於舊版 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 和 worker。types 欄位會列出與記憶體相關聯的特定實作記憶體類型。

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

意見回饋

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

請告訴我們 API 設計

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

回報實作問題

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

顯示支援

您是否打算使用 performance.measureUserAgentSpecificMemory()?你的公開支援有助於 Chrome 團隊將功能列為優先,並向其他瀏覽器供應商顯示支援這些功能的重要性。請在 Twitter 上傳送推文給 @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 上提供