了解如何衡量生产环境中网页的内存用量,以检测回归问题。
浏览器会自动管理网页的内存。每当网页创建对象时,浏览器都会在后台分配一块内存来存储该对象。由于内存是有限资源,浏览器会执行垃圾回收,以检测何时不再需要某个对象并释放底层内存块。
不过,检测并非完美无缺,艾伦·图灵的停机问题证明了完美检测是不可能完成的任务。 因此,浏览器会用“对象可访问”的概念来近似表示“需要对象”。如果网页无法通过其变量和其他可访问对象的字段访问某个对象,则浏览器可以安全地回收该对象。这两个概念之间的差异会导致内存泄漏,如以下示例所示。
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 测试,以评估其对内存的影响并检测内存泄漏。
- 将内存用量与会话时长相关联,以验证是否存在内存泄漏。
- 将内存用量与用户指标相关联,以了解内存用量的整体影响。
浏览器兼容性
目前,只有基于 Chromium 的浏览器(从 Chrome 89 开始)支持该 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 不会立即解析结果 promise,而是会等待下一次垃圾回收。
调用该 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、相关窗口和 Web Worker 的 JavaScript 和 DOM 内存。
breakdown 列表提供了有关已用内存的更多信息。每个条目都描述了内存的某些部分,并将其归因于一组由网址标识的窗口、iframe 和工作器。types 字段列出了与内存关联的实现特定内存类型。
请务必以通用的方式处理所有列表,不要根据特定浏览器进行硬编码。例如,某些浏览器可能会返回空的 breakdown 或空的 attribution。其他浏览器可能会在 attribution 中返回多个条目,表明它们无法区分这些条目中哪个拥有内存。
反馈
Web 性能社区小组和 Chrome 团队非常期待了解您对 performance.measureUserAgentSpecificMemory() 的想法和体验。
请向我们介绍 API 设计
该 API 是否有某些方面未按预期运行?还是缺少实现想法所需的属性?在 performance.measureUserAgentSpecificMemory() GitHub 代码库中提交规范问题,或将您的想法添加到现有问题中。
报告实现方面的问题
您是否发现了 Chrome 实现中的 bug?还是实现与规范不同?在 new.crbug.com 上提交 bug。请务必尽可能详细地说明,提供重现 bug 的简单说明,并将 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。