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

浏览器兼容性

浏览器支持

  • Chrome:89。
  • Edge:89。
  • Firefox:不受支持。
  • Safari:不支持。

来源

目前,只有从 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 不会立即解析结果 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() 函数会计算以毫秒为单位的随机间隔,以便平均每 5 分钟进行一次测量。如果您对该函数背后的数学原理感兴趣,请参阅指数分布

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 工作器的 JavaScript 和 DOM 内存。

breakdown 列表提供了有关所用内存的更多信息。每个条目都描述了内存的某些部分,并将其归因于一组由网址标识的窗口、iframe 和工作器。types 字段会列出与内存关联的特定于实现的内存类型。

请务必以通用方式处理所有列表,切勿根据特定浏览器对假设进行硬编码。例如,某些浏览器可能会返回空的 breakdown 或空的 attribution。其他浏览器可能会在 attribution 中返回多个条目,表示它们无法区分哪个条目拥有内存。

反馈

网站性能社区群组和 Chrome 团队非常期待了解您对 performance.measureUserAgentSpecificMemory() 的看法和使用体验。

请向我们说明 API 设计

API 是否存在无法按预期运行的地方?或者,您是否缺少实现想法所需的属性?在 performance.measureUserAgentSpecificMemory() GitHub 代码库中提交规范问题,或者添加您对现有问题的看法。

报告实现方面的问题

您是否发现了 Chrome 实现中的 bug?还是实现与规范不同?请访问 new.crbug.com 提交 bug。务必提供尽可能详细的信息,提供重现 bug 的简单说明,并将组件设置为 Blink>PerformanceAPIs故障非常适合分享快速简便的重现步骤。

表示支持

您打算使用 performance.measureUserAgentSpecificMemory() 吗?您的公开支持有助于 Chrome 团队确定功能的优先级,并向其他浏览器供应商表明支持这些功能的重要性。请向 @ChromiumDev 发送 tweet 消息,告诉我们您在何处以及如何使用它。

实用链接

致谢

非常感谢 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。

主打图片Unsplash 用户 Harrison Broadbent