使用 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 有何差異。主要差別在於舊版 API 傳回 JavaScript 堆積的大小,而新 API 則會預估網頁使用的記憶體。當 Chrome 與多個網頁 (或同一個網頁的多個執行個體) 共用相同的堆積時,這項差異就變得重要。在這種情況下,舊 API 的結果可能會不可行。由於舊 API 是由實作專屬字詞 (例如「heap」) 定義,因此將應用程式標準化為萬無一失。

另一個差別在於新的 API 會在垃圾收集期間執行記憶體測量。這樣可減少結果中的雜訊,但產生結果可能需要一些時間。請注意,其他瀏覽器可能在不依賴垃圾收集的情況下實作新的 API。

建議用途

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

  • 在新版網頁推出期間進行迴歸偵測,找出新的記憶體流失問題。
  • 對新功能進行 A/B 測試,評估其記憶體影響及偵測記憶體流失情形。
  • 將記憶體用量與工作階段持續時間建立關聯,以確認是否存在記憶體流失。
  • 將記憶體用量與使用者指標建立關聯,藉此瞭解記憶體用量的整體影響。

瀏覽器相容性

瀏覽器支援

  • 89
  • 89
  • x
  • x

資料來源

目前自 Chrome 第 89 版起,此 API 僅支援以 Chromium 為基礎的瀏覽器。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 分鐘。然而,這會造成資料偏誤,因為樣本之間可能會出現記憶體高峰。

以下範例說明如何使用 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 和工作站。types 欄位會列出與記憶體相關聯的實作專屬記憶體類型。

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

意見回饋:

您可以向網路效能社群群組和 Chrome 團隊分享您對 performance.measureUserAgentSpecificMemory() 的想法和使用經驗,

告訴我們 API 設計

API 有沒有正常運作的問題嗎?或者是實現想法時需缺少的屬性?請在 performance.measureUserAgentspecificMemory() GitHub 存放區中回報規格問題,或是將想法新增至現有問題中。

回報導入問題

您在執行 Chrome 時發現錯誤了嗎?還是實作與規格不同?前往 new.crbug.com 回報錯誤。請務必盡可能提供詳細資料、提供重現錯誤的簡易操作說明,並將「元件」設為 Blink>PerformanceAPIsGlitch 適合用來快速分享簡單快速的提案,

展現支持

您打算使用「performance.measureUserAgentSpecificMemory()」嗎?您的公開支援可協助 Chrome 團隊決定功能的優先順序,以及向其他瀏覽器廠商瞭解這項功能有多重要。將推文傳送至 @ChromiumDev,並告訴我們您的使用地點和方式。

實用連結

特別銘謝

非常感謝 Domenic Denicola、Yoav Weiss、Mathias Bynens 進行 API 設計審查,以及 Hannes Payer、Kentaro Hara 和 Michael Lippautz 在 Chrome 中審查程式碼,另外,我也感謝 Per Parker、Philipp Weis、Olga Belomestnykh、Matthew Bolohan 和 Neil Mckay,他們都提供寶貴的意見回饋,幫助我們大幅改善 API。

主頁橫幅Harrison BroadbentUnsplash 網站上提供