使用 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,而不依赖于垃圾回收。

建议的用例

网页的内存用量取决于事件的时间、用户操作和垃圾回收。因此,Memory Measurement API 旨在汇总生产环境中的内存使用情况数据。各个调用的结果没那么实用。应用场景示例:

  • 发布新版本网页期间的回归检测,以捕获新的内存泄漏。
  • 对新功能进行 A/B 测试,以评估其内存影响并检测内存泄漏。
  • 将内存用量与会话时长相关联,以验证是否存在内存泄漏。
  • 将内存用量与用户指标相关联,以了解内存用量的总体影响。

浏览器兼容性

浏览器支持

  • 89
  • 89
  • x
  • x

来源

目前,只有基于 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() 函数会计算一个随机间隔(以毫秒为单位),因此平均每 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 的实现过程中是否发现了错误?或者,其实现方式是否与规范有所不同?在 new.crbug.com 上提交 bug。请务必提供尽可能多的详情,提供有关如何重现 bug 的简单说明,并将 Components 设置为 Blink>PerformanceAPIsGlitch 非常适用于分享轻松快速的重现问题。

表示支持

您打算使用 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。

主打图片,作者:Harrison Broadbent,来源于 Unsplash 用户