关于 sandbox-bench
sandbox-bench 是一项技能,可在 Vercel Sandbox VM 上对 React 和 Next.js 的改动进行配对 A/B 性能测试。它通过端到端测量、按启动实例统计的分析和 CPU 配置文件,帮助维护者评估改动影响。
主要功能
- 01为 React 和 Next.js 进行配对 A/B 测试
- 02提供端到端与 SSR 测试套件
- 03测试前执行正确性门槛检查
- 04按 VM 启动实例进行统计分析
- 05捕获并比较 CPU 配置文件
- 06使用 SQLite 存储并验证运行结果
适用场景
- 01测量 React 或 Next.js PR 的性能影响
- 02比较指定的 React 或 Next.js 版本
- 03查看计时测试后的 CPU 配置文件
- 04重新分析并验证已收集的结果
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel/ next.js --skill sandbox-bench安装后在 Claude Code 中输入/sandbox-bench确认已加载。
详细介绍
Sandbox 基准测试:针对 React 和 Next.js 更改的配对 A/B 性能测试
端到端衡量一项更改的实际价值:将两个修订版本(“实验组”)构建到其他方面完全相同的 Next.js 应用中,通过 Vercel Sandbox VM 上的 bench/render-pipeline 测试框架运行,并采用以 VM 启动为重复单位的配对统计方法进行比较。
所有繁重工作都在 Sandbox VM 上完成;笔记本电脑只负责协调。
脚本位于本文件旁的 scripts/ 目录中,可从任意位置运行。
实验组是 git 引用,会在 react 和 next.js 的缓存克隆中解析;Next 侧默认使用 canary。所有内容都按内容寻址进行缓存:首次使用新的组合时会构建缓存(额外耗时约 45-60 分钟,仅一次);后续运行会直接启动并开始测量。
一次性设置
- 运行
node scripts/config.mjs show——如果结果显示 NOT CONFIGURED,请询问用户 Sandbox VM 应在哪个 Vercel 团队和项目下运行(这些是计费资源;绝不猜测,也绝不使用默认值),然后运行node scripts/config.mjs set team=<slug> project=<name>。 配置位于~/.config/sandbox-bench/config.json中——绝不要将团队/项目名称提交到仓库。 - Vercel CLI 会话必须有权访问该团队。遇到 403 时,停止启动操作(不要通过该会话重试),并检查访问权限是否已恢复:运行
vercel whoami --scope <team-slug>,再执行一个带作用域的读取调用(例如vercel sandbox ls)——授权可能自行失效并恢复,临时 403 完全不需要登录。如果验证仍未通过,请自行在后台任务中运行vercel login <team-slug>(令牌保存在 CLI 会话中,而非用户账户中)。该命令会打开浏览器/设备确认页面——如果打印了 URL,请转发给用户——但等待期间仍要每隔一两分钟重新运行这组验证命令:访问权限通常会在登录流程报告成功之前恢复;验证一旦通过,就终止等待中的登录任务并继续操作。403 服务中断后,预计正在进行的运行已停止: 运行node scripts/bench-status.mjs并执行其恢复操作(如果中断时间较长,测量 VM 将达到约 5 小时的超时限制——这些单元需要重新启动,而不是收集结果)。 - 首次使用时,react 和 next.js 克隆会下载到缓存中(也可以在配置中将
reactRepo/nextRepo指向现有检出目录)。
使用新配置进行首次正式运行前,请先通过 --dry-run 检查计划(只会打印将要执行的操作,不会修改任何内容)。
工作流
1. 确定比较对象
- React PR:使用
--pr <url|number>——会自动计算基线(PR 分支头与 react main 的合并基点)。 - React 引用:使用
--arms base=<ref>,cand=<ref>——基线必须放在前面。对于包含多个提交的分支,基线应是与 main 的合并基点,而不是cand^。 - Next.js PR:使用
--next-pr <url|number>。React 侧默认使用各 Next 引用所包含的版本(也就是将要发布的版本);只有需要将两个实验组固定到同一个特定 React 构建时,才传入--react-ref。 - Next 引用:使用
--next-arms base=<ref>,cand=<ref>。
只有一侧会变化,另一侧在两个实验组中保持一致。正是这种隔离使结果能够归因于该更改——绝不要同时改变两侧。
2. 在耗费基准测试算力前确保正确性
如果某个分支未通过自身测试,那么它的基准测试数据毫无意义。 对于尚未在上游 CI 中通过的任何分支(手动组装的分支、已解决冲突的 cherry-pick、本地提交):
node scripts/sandbox-gate.mjs --arms cand=<ref>
基准测试本身会执行主要门禁:每个 react 分支的提交都必须在 react 仓库中通过 CI,并且会在消耗任何构建或虚拟机资源前自动检查。PR 和主分支历史中的提交通常无需额外操作即可满足此条件。对于本地或尚未推送的引用(没有 CI),请使用 sandbox-gate.mjs 在虚拟机上执行门禁,然后向基准测试传入 --allow-ungated。虚拟机门禁会在生产模式下运行完整测试套件(也就是接受基准测试的渠道)。要判定为 PASS,输出中必须能看到实际测试数量。如果门禁失败,请报告失败情况并停止——不要对有问题的分支进行基准测试。每个分支都在其各自 lockfile 对应的环境中执行门禁。请对门禁输出的确切 sha 进行基准测试(分支引用可能会在门禁和基准测试之间发生变化)。
3. 启动基准测试(后台、非阻塞)
bash -c 'node scripts/sandbox-e2e.mjs --pr <url> --label <slug> \
2>&1 | grep --line-buffered -v "^live "; exit ${PIPESTATUS[0]}'
对于 React PR,同时启动两套测试(作为独立的后台任务;它们会共用 arm 构建和缓存):
bash -c 'node scripts/sandbox-ssr.mjs --pr <url> --label <slug>-ssr \
2>&1 | grep --line-buffered -v "^live "; exit ${PIPESTATUS[0]}'
e2e 套件通过真实的 Next.js 应用测量 Node 路径;ssr 套件测量 react 仓库的 flight-ssr-bench fixture——共 8 个变体(Fizz 和 Flight+Fizz、Node 和 Edge Web Streams、同步和异步),每个变体依次运行,并注入 Flight 脚本,通过 HTTP 服务在 c=1/c=10 下测试。Edge 单元是 ssr 套件的重点(e2e 套件无法覆盖该路径);其中的 Node 和仅 Fizz 单元用于将效果归因到 Flight 层、Fizz 层或流处理管道。两组测试使用的 fixture(工作负载)都固定在同一个 ref,默认是 react main,因此只有 React 构建不同;如果 PR 本身修改了 fixture,启动器会提示,运行结果也不会测量这些修改。Next PR 只运行 e2e 套件。
- 将其作为后台任务运行,并在收到完成通知后继续。绝不要在前台等待;也不要循环轮询——规则针对的是控制流,而非状态转达:读取输出尾部来回答“进展如何”始终可以。
- 测试框架会在内部处理不变量:两组测试在同一个 VM 中运行,按 ABBA 顺序交错执行,按 (vm, run) 配对;远程执行会脱离连接运行(传输中断不会终止测试);每一条结果记录都会记录构建指纹。
live ...行只是用于进度显示的实时估算值。绝不要因为实时 p 值看起来不错就提前停止测试,也绝不要报告实时数值——顺序窥视会制造假阳性。只有最终分析结果才有效。- 默认配置(16 个 VM × 2 次配对运行)符合方法要求;不要为了节省时间而减少 VM 数量——启动次数是推断的计量单位,启动次数越少,区间越宽,而非答案越快。
- 沙箱算力是内部容量,不是预算:启动、重新启动并确认测试时,不必询问成本,也不要为了节省算力而缩减测试规模。
- Next 端默认使用
canary,即最新发布的 canary 版本(启动器会打印其版本和 sha),因此重复基准测试会复用已构建的快照,直到发布新的 canary。 - 实用标志:
--bench-env KEY=VALUE(仅用于基准测试进程的运行时环境变量——不会影响快照的应用构建)、--isolate-routes(用于尾部调查)、--no-profile(跳过默认在计时运行后执行的 CPU 采样)、--prepare(仅构建缓存——当两个单元会共用一个 arm 时使用,以避免重复构建发生竞争)。 - 默认会采集 CPU profile:每个 arm 在计时运行严格结束后执行一次 profile 采样(不会影响测试数值),额外耗费约 45-60 分钟的 VM 墙钟时间,并将标准 V8
.cpuprofile文件写入<runDir>/prof-vm<N>/。跨 VM 的 profile 差异高度稳定(在实际有变化的案例中,观察到 16/16 的符号方向一致),因此一个经过 profile 采样的单元就足以对热点路径排序。分析时注意:按 (functionName, line, column) 聚合——单独的压缩名称可能在不同 bundle 间冲突——而且绝不要按压缩名称比较两组测试(压缩器会在不同构建间重命名);应改为匹配位置或代码片段。 - 该基准测试会运行 Next 的 node-streams 路径(
__NEXT_USE_NODE_STREAMS在构建时会以内联方式设为 true,用于 node 运行时)。只涉及 EDGE 流配置的 React 修改不会通过端到端测试覆盖,因此基准测试会正确地显示未检测到差异。
4. 像持怀疑态度的数据科学家一样解读结果
目标是了解变更的真实情况,而不是让作者感觉良好。最终分析会按路由/阶段/指标打印启动级均值、±95% CI,以及跨启动的 p 值。遵循 references/methodology.md 中的政策:
- 只有在通过 A/A 验证的团队/配置上,才能根据启动级 p < 0.01 和 CI 作出结论(见方法说明)。
- PR 是一个假设,不是解释。结论只来自分析输出。当数字符合 PR 的说法时,要检查采集的数据是否真的能将该机制与其他可能性区分开来——如果将延迟改善归因于负载变小,就应该同时看到文档字节数的变化;如果字节数没有变化,这个说法就站不住脚,报告中也要说明。
- 使用所有采集到的指标,并指出任何不合理之处:某一指标系列的变化与其他指标相反、没有字节数级别或 RSS 变化的效果、吞吐量变化而延迟没有变化、不同启动之间的符号反转。无法解释的不一致之处应写入报告,而不是束之高阁。
- 方括号中的
within-run p仅用于诊断,不能作为结论。 - 首先检查指纹标头:两个不同的指纹 = 有效的 A/B;“inconsistent fingerprints” = 无效,不报告任何数字。指纹对两个打包器编译后的服务文件进行哈希——仅修改客户端文件的实验组仍可能合法地出现相同指纹和不同版本字符串。
- 会打印每次启动的值;如果不同启动的符号不一致,要明确指出。
- 任何将用于决策的结论,在作为事实陈述前,都必须经过一次独立确认运行。
无需重新运行即可重新分析任何过往运行:
node scripts/bench-analyze.mjs <runDir>。
5. 撰写报告
注明测量对象并附上链接:PR 标题(打印在分析标头中,存储于 meta.json)链接到 PR;对于基准实验组,链接到提交标题。报告开头放一张列出显著单元格的表格,每行都包含效果及其单位、CI 和 p 值:
## [<PR title>](<PR url>) — e2e, Vercel Sandbox (x86 Xeon), <n> boots
Significant (boot-level p < 0.01, A/A-validated):
| cell | effect | 95% CI | p |
|---|---|---|---|
| /dashboard under load | +14.4% throughput (req/s) | ±3.2% | <0.0001 |
| /dashboard serial | −10.7% median latency (ms) | ±0.6% | <0.0001 |
No detected difference: <every cell not in the table, by name>.
Flags: <cells at 0.01 ≤ p < 0.05, sign disagreements across boots,
fingerprint caveats, anything that does not add up>
每个单元格占一行:rps 和中位数表达的是同一回事,因此报告吞吐量数值(只有当 p95 的变化与中位数不同时,才添加 p95 行)。文档指标(原始/gzip/Flight KB)不同时,分别列出——它们是机制证据。如果 Next 一侧早于文档指标测试框架(vercel/next.js#95828),这些单元格就不存在;应明确说明,而不是默默少报指标。注明数字对应的平台。效果大小取决于平台(GC 占比会因 CPU 而异);变化方向和机制可以迁移,百分比则不行。切勿将与噪声相容的差值说成小幅改善或退化——应称为“未检测到差异”。
结果数据库
每次采集的运行都会存入一个 SQLite 文件 ~/.cache/sandbox-bench/results.db——其中只有原始测量数据和产物(CPU 配置文件、日志),且仅由导入器写入,绝不手动写入。启动器会在采集时自动导入并验证;bench-analyze 只读取数据库,不读取其他内容,因此每项统计数据都是数据库的纯函数。报告中的数字须原样取自分析输出——绝不手动重打、重新计算或汇总。
node scripts/bench-db.mjs ls— 列出所有运行及其样本/产物数量。node scripts/bench-db.mjs verify [runId]— 执行完整性检查:SQLite 级别检查、引用完整性检查、每个实验组仅有一个指纹、配对样本数量检查、产物 sha256 校验。使用旧数据前先运行此命令。node scripts/bench-db.mjs export out.db <runId...>— 将指定运行(及其配置文件)导出为独立数据库,以便发送给他人。可用任何 SQLite 工具打开。node scripts/bench-analyze.mjs <runId>— 重新分析数据库中的任意内容;如果传入运行目录参数,会先导入。
让用户了解进展
启动器会在 stdout 上播报运行情况:先报告启动信息(运行目录、
实验组、CI 结论),然后每隔约 2 分钟输出一行进度,其中包含
已收集的行数和各路由效果的中期估算及置信度。启动后立即告知用户
运行目录和预计耗时;如果用户询问进展,告知显著的中期变化;完成
通知到达后,转达最终分析的完整结论。分析会指出本次运行中
not captured on this run 的指标;如果这些指标缺失会限制数据
所能说明的内容,就在结论中重复这一点(文档指标缺失表示负载机制
未经验证,而不是已验证为相同)。
运行进行期间,每次回复开头都要按运行逐项提供一行状态: 读取启动器输出的末尾,并引用最新的进度行。如果会话支持定时唤醒或 提醒,就在每个预期阶段转换时安排检查(实验组构建 -> 实验快照 -> 测量中,之后每测量约 15 分钟检查一次),并发布进度行;如果不 支持,就说明下一次更新何时到达,避免用户不清楚沉默代表什么。
进度行中的中期效果是持续更新的估算值——可以作为进度分享,绝不能 当作定论。
如果启动器进程终止(会话拆除、崩溃),远程 VM 仍会继续执行测量
循环——数据不会丢失。node scripts/bench-collect.mjs <runDir>
会重新连接、等待循环结束、下载结果、清理并分析。在 VM 达到约
5 小时超时前运行它。
故障恢复
- 始终先执行:
node scripts/bench-status.mjs。 会话 重启会悄无声息地杀死后台启动器,而其分离运行的 VM 会继续测量; 已终止启动器的日志末尾仍可能有看起来正常的进度行——绝不能根据 日志末尾或任务输出文件推断进程是否仍在运行。bench-status 会检查 各次运行记录的启动器 pid,并打印对应的恢复操作(running / collect now / relaunch)。在任何预期有任务正在运行的会话开始时、 任何崩溃之后,以及告知用户当前有哪些任务正在运行或未运行之前, 都要运行此命令。启动器崩溃也会记录在运行的 status.json 中 (phase: "failed"加上错误信息)。 - 本地进程中断:远程 VM 会继续分离运行。使用配置的团队和项目
执行
vercel sandbox list来查找它们;轮询每个 VM 的/vercel/sandbox/loop.done,完成后将其results.jsonl用cp下载下来,然后移除 VM 并使用bench-analyze.mjs分析。 - 任何崩溃后遗留的 VM:
node scripts/sandbox-sweep.mjs会列出此技能的 VM(名称匹配 sbench-* 且带有 purpose=sandbox-bench 标签,并且只列出超过 --min-age-hours 的 VM,默认值为 3 小时, 因此绝不会触碰正常运行中的任务);--yes会按列出的准确名称 移除它们。 - 上传或传输不稳定:测试框架会检查制品大小,并在文件被截断时
中止。失败的单元可以安全地重新启动;缓存会让重试成本很低。不要
同时重新启动两个需要构建同一个未缓存实验组的单元——它们会争抢
构建任务;应先使用
--prepare。
费用预期(大型运行前与用户确认)
默认情况下,每个单元约需 18 个 VM(8 个测量 VM + 构建/快照 VM), 冷启动耗时约 1–2 小时,预热后约 30–60 分钟。A/A 校准和确认运行 需要额外单元。VM 费用计入已配置的团队——除单个 PR 检查外,开始前 都要先确认范围。