首页
/ 在 Vercel 生产环境实测 Next.js 性能:bench/vercel 生产基准测试工具全解

在 Vercel 生产环境实测 Next.js 性能:bench/vercel 生产基准测试工具全解

2026-09-04 20:30:48作者:鲍丁臣Ursa

本文围绕 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.local file to ./env and fill in the required VERCEL_TEST_TOKEN and VERCEL_TEST_TEAM values. 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_NAMEbench/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 dev or pnpm build --force.

即如果你改动了 Next.js 源码,必须先在 monorepo 根目录跑过 pnpm devpnpm 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.apphttps://<项目名>-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.jsgenerateProjects(),它用 listr2 组织了两个可并发执行的任务组(concurrent: true),每组内部步骤串行:

Origin 项目(基线):

  1. Resetting project:调用 scripts/reset-project.mjs 中的 resetProject({ teamId, token, projectName, disableDeploymentProtection: true }),在 Vercel 团队下重置/确保项目存在;
  2. copying app:用 cp -f -Rbench/vercel/benchmark-app 复制到 benchmark-app-origin
  3. Set Next.js version in package.json:调用 generatePackageJson(originAppFolder),写入的是当前发布的 Next 版本号
  4. deploying project:执行 Vercel 部署,拿到 origin 部署 URL。

Head 项目(本地改动版):前两步相同(复制到 benchmark-app-head),第 3 步改为 generatePackageJson(headAppFolder, true),即以"本地构建"模式生成 package.json;第 4 步部署拿到 head 部署 URL。

无论成败,bench.js#L78-L82finally 块都会调用 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 所说 "we npm pack the local Next build and add it to the repo" 的具体实现;
  • origin 侧则读取 packages/next/package.jsonversion 字段,安装官方发布版本作为基线。

3.3 部署命令细节

project-utils.js#L129-L185deployProject() 通过 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=1NEXT_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-L97runBenchmark(url) 对每个 URL 发起 500 个请求,并发由 PQueue({ concurrency: 50 }) 限制为 50(bench.js#L16),终端实时打印进度。

4.1 冷启动与热启动的区分

判定逻辑分两部分配合:

  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() 强制页面为动态渲染,保证每次请求都经过服务器函数。

  2. 客户端按响应体归类bench/vercel/gen-request.js 在拿到完整响应体后,以响应体是否包含 HOT 字串判定本次请求属于冷还是热:

    resolve({
      ...response.timings.phases,
      cold: !body.includes('HOT'),
    })
    

4.2 计时与重试

  • 计时使用 @szmarczak/http-timertimer(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 coldBenchmark results for hot 两张对比表。

五、统计与报告:分位数、置信区间和终端图表

bench/vercel/chart.js 负责把原始 TTFB 数组变成可对比的报告。

5.1 指标计算

getMetrics(data)chart.js#L7-L25)对排序后的样本计算:

  • hits:有效样本数;
  • median(p50)、p25p75p95p99 分位数(按 Math.floor((n - 1) * percentile) 取整);
  • avg:算术平均;
  • min / max
  • confidenceInterval:95% 置信区间的误差界,实现为 z * (s / sqrt(n)),其中 z = 1.96chart.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 两条路由:

因此用 -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-pluginwebpack-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.apphttps://<项目名>-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 分布图给出可直接判断优劣的对照结果。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
982
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384