Playwright 浏览器池化:从资源困境到高性能并发

2026/8/27·1 views

别再让每个测试都启动一个浏览器了,这可能是你测试套件最昂贵的习惯

问题的根源:为什么你的测试越跑越慢

在编写 Playwright 自动化测试时,许多团队都经历过这样的场景:每个测试用例独立启动一个浏览器、执行登录、完成操作,然后关闭浏览器。当用例数从几十增长到几百,问题就会集中爆发——30 分钟的测试套件膨胀到近 2 小时,系统资源被大量消耗,测试稳定性也因频繁的浏览器启停而下降。

根本原因在于:浏览器启动和销毁的开销远超你的想象。

一次 Chromium 启动需要 2-5 秒的重 CPU 计算,当多个测试并行时,所有 worker 几乎同时启动浏览器,造成尖锐的 CPU 尖峰,然后进入空闲期——资源利用率极不平衡。

核心概念:Context vs Page vs Browser

在深入池化方案之前,需要理清 Playwright 的三个核心概念:

  • Browser(浏览器实例):最重的资源,启动成本最高。一个 Browser 可以包含多个 Context。
  • Browser Context(浏览器上下文):独立的会话环境,拥有独立的 Cookie、LocalStorage、缓存和权限设置。创建成本远低于 Browser。
  • Page(页面):Context 中的一个标签页,最轻量。

优化的基本思路是: 尽可能复用 Browser,按需创建 Context,轻量化地复用 Page。

方案一:页面池模式

当需要在同一浏览器上下文中并行执行多个任务时,可以预先创建一组页面,形成一个“页面池”:

typescript 复制代码
class PagePool {
  private pool: Page[] = [];
  private available: Page[] = [];
  private size: number;
  private context: BrowserContext;

  constructor(context: BrowserContext, size: number = 3) {
    this.context = context;
    this.size = size;
  }

  async initialize() {
    for (let i = 0; i < this.size; i++) {
      const page = await this.context.newPage();
      this.pool.push(page);
      this.available.push(page);
    }
  }

  async acquire(): Promise<Page> {
    if (this.available.length === 0) {
      // 池子满了,等待或创建新页面
      const page = await this.context.newPage();
      this.pool.push(page);
      return page;
    }
    return this.available.pop()!;
  }

  release(page: Page) {
    // 清理状态,如导航到空白页
    page.goto('about:blank');
    this.available.push(page);
  }
}

这种模式适合需要频繁创建和销毁页面的场景,避免了重复创建页面的开销。

方案二:浏览器实例池(核心方案)

这是最核心的优化手段——维护一组预热好的浏览器实例,请求到来时直接分配,用完归还。

基本实现思路

typescript 复制代码
import { chromium, Browser } from 'playwright';

class BrowserPool {
  private browsers: Browser[] = [];
  private available: number[] = [];
  private maxSize: number;
  private currentSize: number = 0;

  constructor(maxSize: number = 5) {
    this.maxSize = maxSize;
  }

  // 懒加载:首次请求时才创建浏览器
  async acquire(): Promise<Browser> {
    // 如果有可用实例,直接取出
    if (this.available.length > 0) {
      const idx = this.available.pop()!;
      return this.browsers[idx];
    }

    // 未达到上限,创建新实例
    if (this.currentSize < this.maxSize) {
      const browser = await chromium.launch({ headless: true });
      const idx = this.currentSize;
      this.browsers[idx] = browser;
      this.currentSize++;
      return browser;
    }

    // 池子满了,等待可用实例(实际项目中需要实现等待机制)
    throw new Error('No available browser instance');
  }

  release(browser: Browser) {
    const idx = this.browsers.indexOf(browser);
    if (idx !== -1) {
      this.available.push(idx);
    }
  }

  async shutdown() {
    for (const browser of this.browsers) {
      if (browser) await browser.close();
    }
  }
}

进阶功能:预热(Pre-warming)

生产环境中,更推荐在服务启动时预热一批浏览器实例,而不是等到请求到来时才创建。这样可以避免第一个请求的冷启动延迟,同时让 CPU 负载更平滑。

Playwright 官方在 run-server 命令中已经提供了 --pre-warm 选项:

bash 复制代码
# 启动服务时预热 4 个浏览器
npx playwright run-server --port 3333 --pre-warm 4

# 测试 Worker 连接时直接获得已就绪的浏览器实例
PW_TEST_CONNECT_WS_ENDPOINT=ws://localhost:3333/ npx playwright test

预热 + 串行补充的机制让 CPU 保持平稳负载,而不是在尖峰和闲置之间交替。

方案三:分布式浏览器网格

当单机资源不够时,需要将浏览器池扩展到多台机器。playwright-distributed 提供了一个开箱即用的方案:

bash 复制代码
# 启动代理 + Redis + 浏览器 Worker
docker compose up -d

客户端通过 WebSocket 连接即可获得隔离的浏览器上下文:

typescript 复制代码
import { chromium } from 'playwright';

const browser = await chromium.connect('ws://localhost:8080');
const context = await browser.newContext();
const page = await context.newPage();

这个方案支持:

  • 横向扩展:随时添加或移除 Worker
  • 多浏览器类型:通过 URL 参数切换 Chrome/Firefox/WebKit
  • 单端点接入:所有语言和团队共享同一个接入点

生产环境注意事项

资源规划

实际测试中,8 核 16G 的机器上每个浏览器进程约占用 150-250MB 内存,池大小设为 5 是比较稳妥的选择。想跑更多并发,建议上容器拆分。

会话隔离

Browser Context 之间天然隔离(Cookie、Storage、缓存互不干扰),可以安全地并行执行不同任务。多个客户端可以共用一个 Browser 实例,但各自拥有独立的 Context。

健康检查与自动回收

生产级池化需要支持:

  • 健康检查:定期验证浏览器响应性,自动替换死节点
  • 最大使用次数:浏览器复用超过阈值后主动回收,防止内存泄漏
  • 闲置超时:长时间空闲的实例自动关闭释放资源

状态快照复用

对于需要登录状态的任务,可以将认证状态保存为文件,后续 Context 直接加载,避免重复登录:

typescript 复制代码
// 首次登录后保存状态
await context.storageState({ path: 'auth-state.json' });

// 后续测试复用
const context = await browser.newContext({
  storageState: 'auth-state.json'
});

写在最后

浏览器池化不是锦上添花的优化,而是规模化 Playwright 应用的必选项。无论你选择页面池、实例池还是分布式网格,核心目标一致:减少浏览器启动次数,提高资源利用率,让测试跑得更快、更稳。

从最简单的懒加载池开始,逐步引入预热、健康检查和回收策略,你的测试套件将不再被资源瓶颈拖累。