使用 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 測試,以評估其記憶體影響及偵測記憶體流失情形。
  • 將記憶體用量與工作階段持續時間相關聯,以驗證是否存在記憶體流失。
  • 找出記憶體用量與使用者指標之間的關係,藉此瞭解記憶體用量的整體影響。

瀏覽器相容性

瀏覽器支援

  • 89
  • 89
  • x
  • x

來源

自 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 分鐘一次。但這會導致資料產生偏誤,因為樣本之間可能會出現記憶體高峰。

以下範例說明如何使用 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 回報錯誤。請務必盡量附上詳細資料、提供重現錯誤的簡單操作說明,並將「Components」(元件) 設為 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 Broadbent 提供,於 Unsplash 提供