用例太多跑太久:Playwright 的 sharding 分片并行怎么切
用例攒到几百条之后,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.ts 的 filterForShard() 函数里。这个函数的注释把粒度写得很直白: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.20,docs/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 上才切 blob。docs/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),所以十分片以上时会得到 01、02 这样的编号,字典序排下来仍然是对的。同一个文件里还有一处硬校验:如果你自定义了 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>,后者可以逗号分隔多个,文档里列出的可选值包含 html、json、junit、github、blob 等。
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,合并结果就是failed;timedout、interrupted依次退让。合并出来的 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.ts 的 mergeConfigureEvents() 里写着:没有 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,但关于「缺失分片会不会被发现」这一点,我们在仓库文档里没有找到明确说明。合并出来的报告是否覆盖了全部用例,还是要靠你自己核对。
九、怎么验证配对了
分片配错最讨厌的地方是它不报错,只是悄悄少跑一些用例。几个可自查的点:
- 看每个 shard 的文件名。 各 job 产出的 blob 文件应当形如
report-<hash>-<shard_number>.zip,编号互不相同。如果几份文件名完全一样,说明--shard没有真正传进去。 - 看 hash 是不是一致。 文档写明这个 hash 由
--grep、--grepInverted、--project、TestConfig.tag以及命令行上的文件过滤参数算出来,目的是让不同命令行参数产生不同但稳定的报告名。各 shard 的 hash 应当相同——不同就说明某个 job 的过滤参数和别人不一样,它跑的根本不是同一批用例。 - 数总条数。 合并后报告里的用例总数,应当和不分片跑一次的总数对得上。这是最直接的一道检查。
- 多环境跑同一套用例时用
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 签名、配置项与默认值随版本变动,请以仓库最新内容为准。
本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。