在 Vercel 生产环境实测 Next.js 性能:bench/vercel 生产基准测试工具全解
本文围绕 Next.js 仓库中 bench/vercel 生产基准测试工具展开:它把本地构建的 Next.js 通过 npm pack 打包后部署到 Vercel,并与官方发布版本进行 A/B 对照压测,输出冷/热启动下的 TTFB 分位数、置信区间与终端图表。读完本篇,你将完整掌握该工具的环境准备、命令行参数、两套部署项目(origin/head)的生成机制、请求与计时原理,以及统计口径的源码级细节。
一、这个工具解决什么问题
bench/vercel/README.md 对工具的定位只有一句话,但非常关键:
This script allows you to measure some performance metrics of your local build of Next.js on production by uploading your current build to Vercel with an example app and running some basic benchmarks on it.
也就是说,它不是在本机 next start 上跑基准,而是把你当前本地改动的 Next.js 构建产物上传到 Vercel 生产环境,用一个内置的示例应用跑真实的云函数请求,从而测量生产环境下的性能指标。这解决了一个本地压测无法回答的问题:平台运行时(Vercel 的函数调度、冷启动、边缘网络)下你的改动到底有没有变快或变慢。
工具的核心思路是对照实验:
- origin 项目:使用仓库当前
packages/next的已发布版本号,即与官方发布行为等价的基线; - head 项目:使用你本地
npm pack出来的构建产物,即待验证的改动版本。
两者部署在同一团队的两个 Vercel 项目里,分别压测后按分位数对比,差异一目了然。
二、环境准备与配置
2.1 前置要求
README 明确列出的唯一外部依赖是 Vercel CLI。除此之外还需要一个可用于部署的 Vercel 团队与 Token。
2.2 环境变量文件
README 的 Setup 章节写道:
Rename the provided
./env.localfile to./envand fill in the requiredVERCEL_TEST_TOKENandVERCEL_TEST_TEAMvalues. You can find and generate those from vercel.com.
当前仓库中实际提供的模板文件是 bench/vercel/.env.dev,其中包含三个字段:
# The Vercel team you want to deploy the project too
VERCEL_TEST_TEAM=
# The corresponding Vercel token
VERCEL_TEST_TOKEN=
# The Vercel project you want to deploy the test project too
VERCEL_TEST_PROJECT_NAME=
VERCEL_TEST_PROJECT_NAME 在 bench/vercel/project-utils.js 中读取,并由此派生出两套项目名与团队、Token、可选的 Edge 函数桥接包名:
export const TEST_PROJECT_NAME = process.env.VERCEL_TEST_PROJECT_NAME
const ORIGIN_PROJECT_NAME = TEST_PROJECT_NAME + '-origin'
const HEAD_PROJECT_NAME = TEST_PROJECT_NAME + '-head'
const TEST_TEAM_NAME = process.env.VERCEL_TEST_TEAM
const TEST_TOKEN = process.env.VERCEL_TEST_TOKEN
const VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG =
process.env.VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG
各变量含义:
| 变量 | 用途 |
|---|---|
VERCEL_TEST_TEAM |
部署目标团队,同时作为 vercel --scope 参数 |
VERCEL_TEST_TOKEN |
Vercel 认证 Token,注入子进程环境变量 TOKEN |
VERCEL_TEST_PROJECT_NAME |
测试项目基名,实际项目名为其加 -origin / -head 后缀 |
VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG |
可选;设置后会作为 --build-env 传给部署,用于指定 Edge 函数桥接包 |
[bench/vercel/.gitignore](https://gitcode.com/GitHub_Trending/next/next.js/blob/ec847d821698a6973c9459c8060281ad4d6f09cd/bench/vercel/.gitignore?utm_source=gitcode_repo_files) 将 .env、*.tgz、.next、.vercel 都排除在版本控制之外,因此本地的 Token 文件与打出的 Next.js tarball 都不会误提交。
2.3 安装与运行
按 README,进入 bench/vercel 后执行:
pnpm install
pnpm bench
其中 bench 脚本定义在 bench/vercel/package.json 中,即 node bench.js。该子项目依赖的第三方库包括 commander(CLI 参数)、p-queue(并发控制)、listr2(步骤清单)、@szmarczak/http-timer(请求计时)、asciichart / downsample-lttb(终端图表降采样)等。
关键前提:README 特别强调——
Note: if you made some changes to Next.js, make sure you compiled them by running at the root of the monorepo either
pnpm devorpnpm build --force.
即如果你改动了 Next.js 源码,必须先在 monorepo 根目录跑过 pnpm dev 或 pnpm build --force,确保 packages/next 的构建产物包含你的改动,否则 head 项目打包进去的仍是旧产物,基准结果没有意义。
2.4 CLI 命令行参数
bench/vercel/bench.js 用 commander 定义了三个选项:
program.option('-p, --path <path>')
program.option('-s, --skip-build', 'Skip build step')
program.option('-f, --force-crash', 'Force function crash')
| 参数 | 作用 | 源码行为 |
|---|---|---|
-p, --path <path> |
只压测指定路径 | 拼接到两个部署 URL 后,如 bench.js#L51-L52 |
-s, --skip-build |
跳过构建/部署步骤 | 直接使用 https://<项目名>-origin.vercel.app 与 https://<项目名>-head.vercel.app 这两个固定 URL 进行压测,见 bench.js#L44-L49 |
-f, --force-crash |
强制函数崩溃 | 导出 forceCrash 标志,部署时追加 --env CRASH_FUNCTION=1,用于复现崩溃恢复场景 |
三、工作流程:两套对照部署项目如何生成
README "How it works" 一节概括了四步:用 Vercel CLI 建项目 → npm pack 本地 Next 构建并加入仓库 → 上传 Vercel 构建 → 拿到部署 URL 后跑测试。下面逐条展开源码实现。
3.1 并行生成 Origin 与 Head 项目
入口是 bench/vercel/project-utils.js 的 generateProjects(),它用 listr2 组织了两个可并发执行的任务组(concurrent: true),每组内部步骤串行:
Origin 项目(基线):
Resetting project:调用 scripts/reset-project.mjs 中的resetProject({ teamId, token, projectName, disableDeploymentProtection: true }),在 Vercel 团队下重置/确保项目存在;copying app:用cp -f -R把 bench/vercel/benchmark-app 复制到benchmark-app-origin;Set Next.js version in package.json:调用generatePackageJson(originAppFolder),写入的是当前发布的 Next 版本号;deploying project:执行 Vercel 部署,拿到 origin 部署 URL。
Head 项目(本地改动版):前两步相同(复制到 benchmark-app-head),第 3 步改为 generatePackageJson(headAppFolder, true),即以"本地构建"模式生成 package.json;第 4 步部署拿到 head 部署 URL。
无论成败,bench.js#L78-L82 的 finally 块都会调用 cleanupProjectFolders(),用 rm -rf 清掉两个临时复制目录,只保留原始 benchmark-app。
3.2 本地 Next.js 的 npm pack
"head" 侧使用本地构建的关键在 bench/vercel/generate-package-json.js:
export async function generatePackageJson(folder, withLocalNext = false) {
// ...
const currentVersions = await getCurrentRootReactPackagesVersions()
packageJson.dependencies['react'] = currentVersions.react
packageJson.dependencies['react-dom'] = currentVersions['react-dom']
if (withLocalNext) {
packageJson.dependencies.next = await packNextBuild(folder)
} else {
packageJson.dependencies.next = await getCurrentNextVersion()
}
// 写回 package.json
}
export async function packNextBuild(folder) {
const process = await execa('npm', [
'pack',
'../../packages/next',
`--pack-destination=${folder}`,
])
return `file:./${process.stdout}`
}
要点:
- React / react-dom 版本从 monorepo 根
package.json的 devDependencies 读取,保证两端应用运行时一致,排除 React 版本差异对结果的干扰; - head 侧执行
npm pack ../../packages/next --pack-destination=<应用目录>,把本地产物打成.tgz,并将依赖写为file:./xxx.tgz的本地文件引用——这就是 README 所说 "wenpm packthe local Next build and add it to the repo" 的具体实现; - origin 侧则读取 packages/next/package.json 的
version字段,安装官方发布版本作为基线。
3.3 部署命令细节
project-utils.js#L129-L185 的 deployProject() 通过 execa 驱动 Vercel CLI,分两步:
# 1. 关联项目
vercel link -p <projectName> --confirm --scope <team>
# 2. 部署
vercel deploy \
--build-env NEXT_PRIVATE_TEST_MODE=1 \
--build-env NEXT_TELEMETRY_DISABLED=1 \
--force \
--scope <team>
从源码可以看到几个细节:
- 子进程环境注入了
TOKEN(即VERCEL_TEST_TOKEN),实现非交互式认证; --build-env NEXT_PRIVATE_TEST_MODE=1与NEXT_TELEMETRY_DISABLED=1会在远端构建环境生效,前者是 Next.js 内部测试模式开关,后者关闭遥测,避免部署被遥测行为干扰;- 若设置了
VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG,会额外追加--build-env VERCEL_EDGE_FUNCTIONS_BRIDGE_PKG=<值>,用于指定构建时使用的 Edge 函数桥接包(见 project-utils.js#L158-L163); - 若 CLI 传了
-f,追加--env CRASH_FUNCTION=1,触发示例应用的崩溃逻辑(见第五节); - 部署成功时 stdout 输出的部署 URL 被直接返回给
generateProjects()。
四、请求生成:500 个请求的计时与冷/热判定
拿到两个部署 URL 后,bench.js#L84-L97 的 runBenchmark(url) 对每个 URL 发起 500 个请求,并发由 PQueue({ concurrency: 50 }) 限制为 50(bench.js#L16),终端实时打印进度。
4.1 冷启动与热启动的区分
判定逻辑分两部分配合:
-
应用侧打标记:示例页面维护一个模块级标志
Math.hot。首次(冷)请求时该标志为false,页面响应COLD并在响应后置位为true;后续(热)请求响应HOT。Pages Router 版实现见 bench/vercel/benchmark-app/pages/index.js,App Router 版见 bench/vercel/benchmark-app/app/rsc/page.js:// pages/index.js if (!('hot' in Math)) Math.hot = false export default function page({ hot }) { return `${hot ? 'HOT' : 'COLD'}` } export async function getServerSideProps() { const wasHot = Math.hot Math.hot = true // ... return { props: { hot: wasHot } } }App Router 版通过
cookies()强制页面为动态渲染,保证每次请求都经过服务器函数。 -
客户端按响应体归类:bench/vercel/gen-request.js 在拿到完整响应体后,以响应体是否包含
HOT字串判定本次请求属于冷还是热:resolve({ ...response.timings.phases, cold: !body.includes('HOT'), })
4.2 计时与重试
- 计时使用
@szmarczak/http-timer:timer(request)之后,response.timings.phases会给出wait/dns/tcp/tls/firstByte等阶段耗时,其中firstByte(TTFB)是核心指标; - 非 200 状态码视为失败并 reject(gen-request.js#L27-L29);
- 外层
genRetryableRequest对失败请求最多重试 10 次,仍失败才抛出Failed to fetch <url>, too many retries,保证偶发网络抖动不会污染数据集。
4.3 异常值过滤
结果汇总时对每条记录用两个条件过滤(bench.js#L62-L77):
(r) => r.cold && r.firstByte <= TTFB_OUTLIERS_THRESHOLD && r.firstByte
TTFB_OUTLIERS_THRESHOLD = 1500(毫秒):TTFB 超过 1.5s 的样本被视为离群值剔除。随后分别打印 Benchmark results for cold 与 Benchmark results for hot 两张对比表。
五、统计与报告:分位数、置信区间和终端图表
bench/vercel/chart.js 负责把原始 TTFB 数组变成可对比的报告。
5.1 指标计算
getMetrics(data)(chart.js#L7-L25)对排序后的样本计算:
hits:有效样本数;median(p50)、p25、p75、p95、p99分位数(按Math.floor((n - 1) * percentile)取整);avg:算术平均;min/max;confidenceInterval:95% 置信区间的误差界,实现为z * (s / sqrt(n)),其中z = 1.96(chart.js#L32-L41)。
5.2 对比表与图表
printBenchmarkResults({ origin, head }, metricSelector) 先按冷/热选择器筛选出 TTFB 序列,再对 origin 与 head 各算一份指标,并逐指标计算 delta = head - origin(含 min、max、avg、median、p25/p75/p95/p99),通过 console.table 输出三列对照表——delta 为负代表本地改动更快。
随后把两组数据各自降采样(downsample-lttb,基于排序后数据,点数不超过终端宽度减 15 的列数),用 asciichart.plot 以蓝(origin)红(head)双色绘制高度 15 行的终端 ASCII 分布图,直观对比两组 TTFB 分布。
六、基准应用与强制崩溃测试
6.1 双 Router 覆盖
bench/vercel/benchmark-app 同时包含 Pages Router 与 App Router 两条路由:
- bench/vercel/benchmark-app/pages/index.js:
/路径,getServerSideProps每次执行; - bench/vercel/benchmark-app/app/rsc/page.js:
/rsc路径,RSC 动态页面。
因此用 -p /rsc 可以单独压测 App Router 的 RSC 渲染路径,不带 --path 则默认压测首页。根布局 bench/vercel/benchmark-app/app/layout.js 保持最小化,避免额外渲染成本影响 TTFB。
其 bench/vercel/benchmark-app/next.config.js 开启了 experimental.appDir: true,并在 ANALYZE 环境变量存在时挂载 webpack-stats-plugin 与 webpack-bundle-analyzer,用于离线分析服务端/客户端 bundle(与 TTFB 压测本身无关,是辅助调试配置)。
6.2 强制崩溃(-f, --force-crash)
两条路由都内置了崩溃钩子,例如 benchmark-app/app/rsc/page.js:
// crash the server after responding
if (process.env.CRASH_FUNCTION) {
setTimeout(() => {
throw new Error('crash')
}, 500)
}
配合 -f 参数(部署时注入 CRASH_FUNCTION=1),函数会在响应发出之后抛错崩溃,用于观察平台在函数崩溃场景下的重启与恢复对后续请求 TTFB 的影响。Pages 版延迟为 700ms,App 版为 500ms。
七、使用方式小结与适用边界
一次完整流程汇总:
# 1. monorepo 根目录,确保本地改动已编译
pnpm dev # 或 pnpm build --force
# 2. 进入 bench/vercel,准备环境变量
# 将模板(仓库中为 .env.dev 所列字段)保存为环境变量文件,填入
# VERCEL_TEST_TEAM / VERCEL_TEST_TOKEN / VERCEL_TEST_PROJECT_NAME
cd bench/vercel
pnpm install
pnpm bench # 完整流程:构建两套项目 + 压测
node bench.js -s # 跳过部署,直接压测已存在的 origin/head 部署
node bench.js -p /rsc # 只压测 /rsc(App Router RSC)
node bench.js -f # 开启强制崩溃场景
适用前提与限制(均来自源码事实):
- 必须拥有可用的 Vercel 团队与 Token,且本机装有 Vercel CLI——这是一条真实的生产部署链路,会产生实际部署;
- 本地 Next.js 改动必须先经过
pnpm dev/pnpm build --force编译,head 侧打包的是packages/next的 dist 产物; --skip-build模式假定https://<项目名>-origin.vercel.app与https://<项目名>-head.vercel.app两个部署 URL 已经存在;- 统计口径固定为:每端 500 请求、并发 50、TTFB > 1500ms 剔除、按响应体
HOT/COLD区分冷/热两组分别统计; - React 版本始终跟随 monorepo 根
package.json的 devDependencies,两端一致,因此对比结果反映的是 Next.js 构建本身的差异,而非依赖版本差异。
整体而言,bench/vercel 提供了一套"以生产环境为准绳"的 Next.js 性能回归验证手段:origin 基线与 head 本地构建在同一平台上对等压测,用分位数与置信区间而非单次测量来下结论,并通过 console.table 的 delta 列与双色 ASCII 分布图给出可直接判断优劣的对照结果。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00