使用 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 中的对象、未关闭工作器、在数组中累积对象等,很容易引入内存泄漏。如果网页存在内存泄漏,则其内存用量会随着时间的推移而增加,并且网页对用户来说会显得缓慢且臃肿。

解决此问题的第一步是衡量它。新的 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

目前,该 API 仅在基于 Chromium 的浏览器中受支持,从 Chrome 89 开始。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 工作器的 JavaScript 和 DOM 内存。

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

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

反馈

Web Performance Community Group 和 Chrome 团队非常希望了解您对 的想法和体验 performance.measureUserAgentSpecificMemory()

向我们介绍 API 设计

API 是否存在某些方面无法按预期运行?或者,您是否需要实现自己的想法,但缺少某些属性?在 performance.measureUserAgentSpecificMemory() GitHub 代码库中提交规范问题,或将 您的想法添加到现有问题中。

报告实现问题

您是否发现 Chrome 的实现存在 bug?或者,实现是否与规范不同?请在 new.crbug.com 中提交 bug。请务必 尽可能详细地说明 bug,提供重现 bug 的简单说明,并将 组件 设置为 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。