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

了解如何测量网页在生产环境中的内存用量,以检测性能下降问题。

布伦丹·肯尼
Brendan Kenny
乌兰·德根巴耶夫
乌兰·德根巴耶夫

浏览器会自动管理网页内存。每当网页创建对象时,浏览器都会“在后台”分配一块内存来存储该对象。由于内存是一种有限的资源,因此浏览器会执行垃圾回收来检测何时不再需要某个对象,并释放底层内存块。

但该检测并不完美,而事实证明完美的检测是一项不可能完成的任务。因此,浏览器将“需要一个对象”的概念近似于“对象可到达”这一概念。如果网页无法通过对象的变量和其他可访问对象的字段访问某个对象,则浏览器可以安全地收回该对象。这两种概念之间的区别会导致内存泄漏,如以下示例所示。

const object = {a: new Array(1000), b: new Array(2000)};
setInterval(() => console.log(object.a), 1000);

在这里,不再需要较大的 b 数组,但浏览器不会回收该数组,因为仍然可以在回调中通过 object.b 访问该数组。因此,较大数组的内存会泄漏。

内存泄漏在 Web 上很常见。引入错误的方法很简单,包括忘记取消注册事件监听器、从 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 测试,以评估其内存影响并检测内存泄漏。
  • 将内存用量与会话时长关联起来,以验证是否存在内存泄漏。
  • 将内存用量与用户指标关联起来,以了解内存用量的总体影响。

浏览器兼容性

浏览器支持

  • 89
  • 89
  • x
  • x

来源

目前,只有基于 Chromium 的浏览器才支持该 API(从 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() 函数会计算一个以毫秒为单位的随机间隔,以确保平均每 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 中返回多个条目,表示它们无法区分其中哪个条目拥有内存。

反馈

Web 性能社区小组和 Chrome 团队非常期待听到您对 performance.measureUserAgentSpecificMemory() 的看法和体验。

向我们介绍 API 设计

是否存在 API 无法正常运行的问题?或者说,你是否缺少一些属性来实现你的构想?您可以在 performance.measureUserAgentSpecificMemory() GitHub 代码库中提交规范问题,也可以将您的想法添加到现有问题中。

报告实施方面的问题

您是否发现了 Chrome 实现方面的错误?或者实现方式是否不同于规范?请在 new.crbug.com 提交 bug。请务必提供尽可能多的详细信息,提供重现 bug 的简单说明,并将组件设置为 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。

主打图片Unsplash 用户 Harrison Broadbent