使用 measureUserAgentSpecificMemory() 监控网页的总内存用量

了解如何衡量生产环境中网页的内存用量,以检测回归问题。

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 测试,以评估其对内存的影响并检测内存泄漏。
  • 将内存用量与会话时长相关联,以验证是否存在内存泄漏。
  • 将内存用量与用户指标相关联,以了解内存用量的整体影响。

浏览器兼容性

Browser Support

  • Chrome: 89.
  • Edge: 89.
  • Firefox: not supported.
  • Safari: not supported.

Source

目前,只有基于 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。