返回

Playwright Blob 未清除问题解析与解决方案

javascript

Playwright 浏览器中 Blob 未清除问题解析

问题现象

在使用 Playwright 进行浏览器自动化测试时,发现通过 chrome://blob-internals 查看,页面生成的 Blob 对象没有像预期那样被及时清除。 在没有 Playwright 的情况下,当打开 maps.google.com 一段时间后,活动的 Blob 数量会逐渐降低到1个以内;然而,在 Playwright 控制的浏览器中,即使等待较长时间,仍旧存在大量活动的 Blob,这表明 Blob 对象并未正确释放。这可能会引发内存泄漏,长此以往对测试稳定性带来潜在影响。

问题根源

通常,浏览器内部会主动清理不再使用的 Blob 对象。Playwright 虽然能够控制浏览器的行为,但一些情况下其环境配置可能会导致 Blob 的释放机制受影响。 这类问题背后的原因较为复杂,主要可能是以下几种:

  • 页面缓存或状态维护 : Playwright 可能会在测试会话期间更积极地缓存某些资源或维护一些中间状态,这些资源中就包含 Blob 对象,使得它们不能及时被标记为可回收的垃圾。这在某些特殊情况下能加速测试运行速度,但也会产生意外的资源消耗。
  • 浏览器的特定行为差异 :不同浏览器版本、不同的设置选项或不同浏览器运行模式(例如有头模式或无头模式)之间可能存在内部行为差异。这些差异有可能影响垃圾回收机制,导致 Blob 延迟清理。
  • Playwright 环境中的某些因素 :Playwright 环境可能默认启用了一些与浏览器内部垃圾回收机制不完全兼容的功能。
  • 事件监听器的影响 :Blob 通常与一些事件监听器关联,如果 Playwright 控制下某些事件处理没有妥善进行,有可能造成 Blob 的引用未被及时解除。
  • Blob 相关组件的异常行为 :Blob 对象创建和管理可能与特定浏览器组件有关,例如 media 或是 Canvas 元素,某些极端使用情况下,它们内部也可能存在异常或死锁,最终影响Blob 的释放。

解决方案与步骤

方案一:显式清理浏览器上下文

浏览器上下文包含独立的浏览器会话环境,例如存储缓存,cookie等。通过创建新的浏览器上下文可以隔离问题影响。
在测试完成之后显式地关闭当前的浏览器上下文可能可以解决这个问题。这将强制清除上下文中遗留的 Blob。

import { chromium } from "@playwright/test";

( async () => {
  let options = {
    headless: false
  }

  const browser = await chromium.launch(options);
  // 使用using包裹context, 在函数体执行完毕后销毁 context
  await using context = await browser.newContext();
  const mapsPage = await context.newPage();
  await mapsPage.goto('https://maps.google.com');

  await new Promise(resolve => setTimeout(resolve, 1000 * 60));
  
  const blobsPage = await context.newPage();
  await blobsPage.goto('chrome://blob-internals');

  await new Promise(resolve => setTimeout(resolve, 1000 * 30));

  await browser.close();
})();
  • 原理 : using语句在上下文销毁的时候会执行析构函数,使得不再需要的资源能够快速被浏览器清除。
  • 操作步骤 : 用 await using 替换 await browser.newContext(), 注意 JavaScript 需要使用 node --harmony-using <filename>.js 或者配置 TypeScript 才能运行包含using的脚本.

方案二:启用浏览器硬性GC策略

可以使用 Playwright 配置来激活 Chrome 的强制垃圾回收机制,这有时能更快地清理掉 Blob 相关的资源。 这需要设置一些命令行标记传递给 Chromium 浏览器。

import { chromium } from "@playwright/test";

( async () => {
  let options = {
      headless: false,
      args: [
      '--js-flags="--expose-gc"',
    ]
    
  }

  const browser = await chromium.launch(options);
  const context = await browser.newContext();

  const mapsPage = await context.newPage();
  mapsPage.goto('https://maps.google.com');

    await new Promise(resolve => setTimeout(resolve, 1000 * 60));

  await context.evaluate(() => {
    if (window.gc) {
        window.gc()
    }
  })

  const blobsPage = await context.newPage();
  blobsPage.goto('chrome://blob-internals');

    await new Promise(resolve => setTimeout(resolve, 1000 * 30));


  await browser.close();
})();
  • 原理js-flags="--expose-gc" 启动项使 window.gc 可以直接被网页调用,调用window.gc() 时会强制进行一次垃圾回收,有一定几率清理 Blob。注意这个机制并不保证一定能释放 Blob。
  • 操作步骤 :将 --js-flags="--expose-gc" 添加到启动参数中。同时,使用 context.evaluate 执行浏览器端的 javascript,来尝试调用 window.gc() 进行一次垃圾回收操作。

方案三:手动释放资源(谨慎使用)

当浏览器本身的回收策略不足够及时的时候,有时候也需要在代码中更主动地去释放对象和解除事件监听器,尤其在频繁操作多媒体数据的情况下。但是这个方案依赖于页面的代码质量和可维护性。

// 这里示例代码依赖于特定的页面操作,请按照实际情况修改。
const mediaElement = await mapsPage.$('video') // 获取视频对象
if (mediaElement) {
  await mapsPage.evaluate(el => {
    el.pause(); // 暂停播放, 如果是 canvas 或 webgl,考虑清除绘制内容或重置。
    el.src = "";//清除media 对象引用
  }, mediaElement)

    // 可以添加更多的针对性释放方法, 例如  revokeObjectURL 如果有的话。

}
  • 原理 : 主动停止可能引发Blob 的元素操作并解除引用,这有可能减少 Blob 的存在。
  • 操作步骤 : 针对具体测试页面, 找出相关生成 Blob 元素操作的 API 并执行。务必在详细了解其行为后,采取释放措施。

额外的安全建议

  • 定期审查 : 对可能产生 Blob 资源的页面操作进行定期审查,确保资源及时释放。
  • 资源监控 : 利用 Playwright 的监控能力和浏览器的开发者工具来实时查看内存消耗,并对异常进行监控,并提前采取预防措施。
  • 控制作用域 :尽量将Blob 相关代码的作用域进行限制,比如尽早的在作用域结束的地方清理对象。
  • 谨慎操作 : 如无必要,不要使用 --js-flags="--expose-gc" 或进行其他强制垃圾回收的操作。 这种操作本身就是不稳定和具有侵入性的。 尽量从优化自身代码入手。

Blob 的管理是一项复杂任务。Playwright 作为一种测试框架本身,可能不会暴露出完全掌控其运行行为的API,以上方法虽然无法完全保证问题的解决,却能有效地缓解相关问题,有助于在测试和生产环境中避免不必要的内存风险。