瞭解如何評估正式版網頁的記憶體用量,偵測迴歸問題。
瀏覽器會自動管理網頁的記憶體。每當網頁建立物件時,瀏覽器都會在「幕後」分配一塊記憶體來儲存物件。由於記憶體是有限資源,瀏覽器會執行垃圾收集,偵測何時不再需要物件,並釋出基礎記憶體區塊。
不過,偵測功能並非完美無缺,艾倫·圖靈的停機問題證明瞭完美偵測是不可能的任務。因此,瀏覽器會將「需要物件」的概念近似於「可連線至物件」的概念。如果網頁無法透過其變數和其他可連線物件的欄位連線至物件,瀏覽器就能安全地回收物件。這兩個概念之間的差異會導致記憶體流失,如下例所示。
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 以上版本等 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 分鐘取樣一次。不過,這會導致資料偏誤,因為樣本之間可能會出現記憶體用量高峰。
以下範例說明如何使用卜瓦松程序進行不偏誤的記憶體測量,確保樣本在任何時間點發生的機率都相同。
首先,定義一個函式,使用 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、相關視窗和網路 worker 的 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、Kentaro Hara、Michael Lippautz 協助審查 Chrome 中的程式碼。我也要感謝 Per Parker、Philipp Weis、Olga Belomestnykh、Matthew Bolohan 和 Neil Mckay 提供寶貴的使用者意見回饋,大幅改善 API。