用例太多跑太久:Playwright 的 sharding 分片并行怎么切

2026-08-18

用例攒到几百条之后,CI 上那条「Run Playwright tests」的步骤会变成整条流水线里最慢的一节。单机上能调的只有 worker 数,而 worker 数受限于那台机器的核数——再往上加就没有意义了。这时候要接着压时间,只剩一条路:把用例拆开,扔到多台机器上同时跑。

Playwright 仓库把这件事叫 sharding,文档在 docs/src/test-sharding-js.md。这篇就沿着那份文档和 packages/playwright/src/ 下的几个文件走一遍:--shard 到底怎么切、切完为什么报告会散成一堆、blob 报告器在里面担什么角色。

一、先说清楚它解决的是哪一段

docs/src/test-sharding-js.md 的开头写明:Playwright 默认就并行跑测试文件,并且会尽量利用本机的 CPU 核心;sharding 是在这之上再加一层,把测试拆成若干个「shard」,每个 shard 是一个能独立运行的任务,在 CI 里可以各自作为一个 job 跑。

所以边界很清楚:worker 解决的是一台机器内部的并发,shard 解决的是跨机器的并发。 两者叠加,不是二选一。如果你只有一台 runner,加 shard 不会让它变快——分片本身不产生额外的计算资源。

二、前置条件:这是 Test Runner 的能力,不是浏览器 API 的能力

这一步经常被跳过,结果是拿 Python 或 Java 绑定的人翻半天找不到对应写法。

仓库 docs/src/ 目录下,文件名带 -js / -python / -java / -csharp 后缀的是语言专属页。sharding 这一篇的文件名是 test-sharding-js.md——只有 -js 一份,我们在 docs/src/ 下没有找到 Python / Java / .NET 版本的 sharding 文档。--shard 这个选项的定义在 docs/src/test-cli-js.md 的命令行选项表里,它在 docs/src/ 下被提到的其它地方(并行、项目、CI、reporter API 那几页)也全是 Test Runner 侧的文档。也就是说,它属于 Playwright Test 这个 Node.js 上的 test runner,而不是各语言绑定共有的浏览器 API 层。

另外,docs/src/test-sharding-js.md 里合并报告用到的 tag 配置项,在 docs/src/test-api/class-testconfig.md 里标着 since: v1.57——这是仓库里逐字写明的引入版本,用之前先确认你的版本够。

三、--shard=x/y 的参数语义

命令行选项表 docs/src/test-cli-js.md 对它的描述是:

--shard <shard> — Shard tests and execute only the selected shard, specified in the form “current/all”, 1-based, e.g., “3/5”.

两个要点:格式是 当前/总数,并且 1-based,不是从 0 开始。文档里给的四分片例子是这样的:

npx playwright test --shard=1/4
npx playwright test --shard=2/4
npx playwright test --shard=3/4
npx playwright test --shard=4/4

这四条命令是文档里的原文示例,4 只是它举的分片数,不是什么推荐值。

真正决定「哪些用例落到哪个 shard」的逻辑在 packages/playwright/src/runner/testGroups.tsfilterForShard() 函数里。这个函数的注释把粒度写得很直白:sharding 是基于 test group 工作的——可并行的文件按单个测试切分,不可并行的文件整个文件一起切;但分片的平衡依据始终是测试条数,而不是文件数。

实现上它先把所有 group 的测试条数加起来得到总数,按分片数均分,除不尽的余数从前几个 shard 开始每个多分一条;然后按顺序累加,落在 [from, to) 区间里的 group 归当前 shard。注释里还补了一句关键的话:一个 test group 会整体归属于「包含它第一条测试」的那个 shard。这意味着一个跨越边界的 group 不会被劈开,实际分到的条数可能和理论均分值有出入。

这里还有一个源码里有、文档里没有的东西:filterForShard() 支持一个叫 PWTEST_SHARD_WEIGHTS 的权重入参,权重个数和分片总数对不上时会直接抛错。但我们在 docs/src/ 下没有找到对它的任何说明,而 PWTEST_ 前缀在这个仓库里普遍用于内部测试用途——代码里有这个分支,不等于它是对外支持的配置,别把它写进生产流水线。

四、fullyParallel 决定切得均不均

docs/src/test-sharding-js.md 专门有一节讲这个,结论是分片粒度取决于你有没有开 fullyParallel

  • 开了 fullyParallel: true:按单条测试切分,每个 shard 尽量拿到数量相当的测试;
  • 没开:默认按文件粒度,整个测试文件被分配给某个 shard。文档明说,同一个文件在不同 project 之间可能被分到不同的 shard。

后一种情况下,如果你的测试文件胖瘦差得多,就会出现有的 shard 跑一大堆、有的 shard 几乎没事干。文档给的建议是:要么开 fullyParallel: true,要么自己把测试文件拆得小而均匀。

fullyParallel 这个选项在配置的两个层级上都有:docs/src/test-api/class-testconfig.md 里的 TestConfig.fullyParallel 标着 since: v1.20docs/src/test-api/class-testproject.md 里的 TestProject.fullyParallel 标着 since: v1.10。前者管整个 run,后者管单个 project。

五、切完之后报告散了:blob 报告器的角色

分片带来的第一个副作用是报告碎片化。docs/src/test-sharding-js.md 写得很直接:上一节的例子里,每个 shard 都有自己的一份报告。要合成一份,就要引入 blob

文档给的配置写法是:

export default defineConfig({
  testDir: './tests',
  reporter: process.env.CI ? 'blob' : 'html',
});

本地还是 html,CI 上才切 blobdocs/src/test-reporters-js.md 的 Blob reporter 一节说明了它的定位:blob 报告包含这次运行的全部细节,可以在之后被转换成任何其它报告;它的主要用途就是支撑分片报告的合并。文档还提到 blob 里带着 trace、截图 diff 这类附件。

文件名的规则是分片能合并的关键。默认输出目录是 blob-report,文件名形如 report-<hash>.zip,用了 sharding 时是 report-<hash>-<shard_number>.zip——名字里带 shard 编号,所以多个 shard 的文件放进同一个目录不会撞名。在 packages/playwright/src/reporters/blob.ts 里能看到对应的实现 _defaultReportName():它把当前分片号按总数的位数做了零填充(padStart),所以十分片以上时会得到 0102 这样的编号,字典序排下来仍然是对的。同一个文件里还有一处硬校验:如果你自定义了 fileName 而结尾不是 .zip,构造时就直接抛错。

上面这些默认值来自仓库当前的文档与代码,随版本可能变动。

六、合并:merge-reports 与它会抛的错

docs/src/test-sharding-js.md 给的做法是把各 shard 的 blob 文件收进同一个目录(它举的例子是 all-blob-reports),然后跑:

npx playwright merge-reports --reporter html ./all-blob-reports

docs/src/test-cli-js.md 里 merge-reports 的选项只有两个:-c, --config <file>--reporter <reporter>,后者可以逗号分隔多个,文档里列出的可选值包含 htmljsonjunitgithubblob 等。

packages/playwright/src/reporters/merge.ts 里能看到合并时的几处行为,都是排查问题时会撞上的:

  • 目录里只有 .zip 结尾的文件会被读取,读完排序处理;一个都没找到时抛 No report files found in ...
  • 如果某个 blob 是更高版本的 Playwright 生成的,会抛 Blob report ... was created with a newer version of Playwright.。所以各 shard 和合并这一步应当用同一套依赖。
  • 合并后的整体状态由各 shard 的结果聚合:只要有一个 shard 是 failed,合并结果就是 failedtimedoutinterrupted 依次退让。合并出来的 config 里 shard 被置为 null——合并产物不再属于任何一个分片。

七、Windows 与 Linux 混着跑时的那个报错

这一段值得单独拎出来,因为本地 Windows、CI 上 Linux 是很常见的组合。

docs/src/test-sharding-js.md 的 Merge-reports CLI 一节写明:跨操作系统合并报告时,必须显式提供一份 merge config,用来指明哪个目录作为 tests root。

不提供会怎样?packages/playwright/src/reporters/merge.tsmergeConfigureEvents() 里写着:没有 rootDir 覆盖值时,它会把各份 blob 记录的 rootDir 收进一个集合,一旦发现不止一个就抛错,错误文案大意是这些 blob 报告是在不同的测试目录下录制的,无法继续合并,可能是因为来自不同操作系统的机器或使用了不同的 playwright 配置,并提示可以用 -c 指定 merge config 强制合并,同时要求 merge config 里的 testDir 指向真实的测试位置。

对应的解法就是文档里的这个写法:

npx playwright merge-reports --config=merge.config.ts ./blob-reports
export default {
  testDir: 'e2e',
  reporter: [['html', { open: 'never' }]],
};

值得一提的是路径分隔符的处理:源码里 PathSeparatorPatcher 只在提供了 rootDir 覆盖值时才会挂上;没提供时合并逻辑直接取第一份 blob 元数据里的 pathSeparator。把这两处放在一起看就明白了——跨系统合并时那个 -c 不只是绕过报错,它同时决定了路径怎么被规整。

另外一个 Windows 侧容易踩的点:blob 报告的输出目录在写入前会被清空(文档里对 PLAYWRIGHT_BLOB_OUTPUT_DIR 的描述是「Existing content is deleted before writing the new report」,源码 blob.ts 里对应的是写入前的 removeFolders)。所以别在本机把几个 shard 的产物依次往同一个 blob-report 目录里塞,后一次会把前一次删掉。

八、边界:哪些用例分不动

docs/src/test-sharding-js.md 明说:Playwright 只能对可以并行运行的测试做分片,默认情况下这意味着按测试文件分片。配合 testGroups.ts 注释里那句「非并行文件整个文件一起切」就能理解——标了串行模式的那一组用例不会被拆开,它整体落在某一个 shard 上。所以一个超大的串行文件会成为分片的下限,加再多 shard 也压不下去。

至于合并报告在极端场景下的行为(比如某个 shard 完全没有产出 blob 文件、或者中途被取消),文档里给的 GitHub Actions 示例用 if: ${{ !cancelled() }}needs: [playwright-tests] 来保证即便部分 shard 失败也能进入合并 job,但关于「缺失分片会不会被发现」这一点,我们在仓库文档里没有找到明确说明。合并出来的报告是否覆盖了全部用例,还是要靠你自己核对。

九、怎么验证配对了

分片配错最讨厌的地方是它不报错,只是悄悄少跑一些用例。几个可自查的点:

  1. 看每个 shard 的文件名。 各 job 产出的 blob 文件应当形如 report-<hash>-<shard_number>.zip,编号互不相同。如果几份文件名完全一样,说明 --shard 没有真正传进去。
  2. 看 hash 是不是一致。 文档写明这个 hash 由 --grep--grepInverted--projectTestConfig.tag 以及命令行上的文件过滤参数算出来,目的是让不同命令行参数产生不同但稳定的报告名。各 shard 的 hash 应当相同——不同就说明某个 job 的过滤参数和别人不一样,它跑的根本不是同一批用例。
  3. 数总条数。 合并后报告里的用例总数,应当和不分片跑一次的总数对得上。这是最直接的一道检查。
  4. 多环境跑同一套用例时用 tag 区分。 docs/src/test-sharding-js.md 的多环境一节给的写法是 tag: process.env.CI_ENVIRONMENT_NAME,注释里的示例值是 "@APIv2"class-testconfig.md 里写明每个 tag 必须以 @ 开头,并且这个 tag 会被 blob 报告带上、参与生成唯一的报告名。这是「多环境」而不是「多分片」的场景,两者别混用。

以上命令与配置片段均按仓库文档中的参数语义组合,未经实测,以仓库最新内容与 --help 的实际输出为准。


本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理, 事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测, 因此不涉及运行速度、稳定性与实际表现的任何描述。 该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。 本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。