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

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

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

但是,检测并不完美,并且已被证明,完美检测是不可能的任务。因此,浏览器使用“对象可访问”的概念来近似“需要对象”的概念。如果网页无法通过其变量和其他可访问对象的字段访问对象,则浏览器可以安全地回收该对象。这两个概念之间的差异会导致内存泄漏,如下例所示。

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 有何不同。主要区别在于,旧 API 返回 JavaScript 堆的大小,而新 API 估计网页使用的内存。当 Chrome 与多个网页(或同一网页的多个实例)共享同一个堆时,这种差异变得很重要。在这种情况下,旧 API 的结果可能会出现任意偏差。由于旧 API 是根据特定于实现的术语(例如“堆”)定义的,因此对其进行标准化是毫无希望的。

另一个区别是,新 API 在垃圾回收期间执行内存测量。这减少了结果中的噪声,但可能需要一段时间才能生成结果。请注意,其他浏览器可能会决定在不依赖垃圾回收的情况下实现新 API。

建议的用例

网页的内存使用量取决于事件发生的时间、用户操作和垃圾回收。这就是为什么内存测量 API 旨在聚合来自生产环境的内存使用情况数据。单个调用的结果不太有用。用例示例

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

浏览器兼容性

浏览器支持

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

来源

目前,该 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://webdev.ac.cn/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 列表提供了有关已用内存的更多信息。每个条目都描述了内存的某个部分,并将其归因于一组由 URL 标识的窗口、iframe 和 Worker。types 字段列出了与内存关联的特定于实现的内存类型。

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

反馈

Web 性能社区组 和 Chrome 团队很乐意听取您对 performance.measureUserAgentSpecificMemory() 的想法和体验。

告诉我们有关 API 设计的信息

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

报告实现问题

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