Vercel 卡在 Building

Build 停在 Building、log 不出新行。先取消、看最后一行 log,再对症修:static generation 里没超时的 fetch、install 死锁、OOM 或 postbuild 钩子。

Vercel 部署列表里那条 Building 已经转了 15 分钟还没动,log 也不出新行。这种”卡住”和”build failed”是两类问题:Failed 会显式抛错并退出,卡住则是进程没死、但没进展。

最快路径:取消这次 build,看最后一行 log,再对照下面的成因。如果尾行是 Generating static pages (X/Y) 而那个数字不再增长,基本就是某个 fetch 没设超时挂住了——这一类占了卡住 build 的大多数。剩下的无非是 npm install 网络死锁、OOM 导致的 GC 反复抖动、或者 postbuild 钩子在等一个 webhook。

别干等:截至 2026 年 6 月,Vercel 的 build 步骤在所有套餐上都有一个硬性的 45 分钟上限(Hobby、Pro、Enterprise 一视同仁,过去”Pro 是 60 分钟”的说法已经不成立)。撞到上限时部署会失败,报 Error: The deployment's build step did not complete within the maximum of 45 minutes。提前取消只是省掉这段等待,诊断步骤完全一样。

常见原因

按命中率从高到低排列。

1. Static generation 卡在某个 page

getStaticProps / Astro 的 getStaticPaths 里调外部 API 没设超时,对方挂起。Next.js 和 Astro 默认串行处理每个 page,一个卡住后面全得等。log 最后一行通常是 Generating static pages (XX/YY),然后再无更新。

Linting and checking validity of types ...
Collecting page data ...
Generating static pages (847/3214) ...
[卡在这里 30 分钟]

如何判断Generating static pages (X/Y) 超过 5 分钟不再增长,几乎可以确定。

2. npm install 阶段网络死锁

npm install 试图拉某个 registry 不可达的包(私有 registry、被墙的 CDN、404 的 git URL),一直重试到 build 超时。log 卡在 install 阶段,后面没东西。

npm warn deprecated ...
[十几分钟不出新行]

如何判断:log 最后一段是 install 阶段,始终没看到框架横幅(▲ Next.jsastro build 等)出现。

3. Build OOM——内存压满但没崩

不是 Command "next build" exited with 137 那种立刻死,而是堆内存压到顶后 GC 反复运行,进程没死但每一步都慢到接近停滞。常见于 TypeScript 项目大规模类型推导、webpack chunk 拆分、Turborepo 并发任务。每个 Vercel build 容器在 Hobby 和 Pro 上都固定为 8192 MB(8 GB)内存 + 32 GB 磁盘(Hobby 给 2 核 CPU,Pro 给 4 核),所以普通的升级套餐买不到更多内存——见 Step 4。

如何判断:log 行的时间戳间隔越来越长(前 5 分钟出几百行,之后每分钟只出 1-2 行),多半是 GC thrashing。要确认,给项目加一个环境变量 VERCEL_BUILD_SYSTEM_REPORT=1,之后 Vercel 会在每次部署都打印一份 build system report,列出峰值内存、隐藏的 OOM 事件,以及 node_modules 和产物的磁盘占用拆解。

4. Postbuild 钩子调用外部服务阻塞

package.json 里有 "postbuild": "node scripts/notify-cdn.js",脚本里 fetch 一个 webhook 没设超时;或者 sitemap 生成器跑全站爬虫卡在某条慢 URL 上。

$ next build && next-sitemap
✓ Generating static pages (1000/1000)
$ next-sitemap
[卡这]

如何判断:log 显示 build 主流程已完成(出现 或 build 完成横幅),但状态还是 Building——一定是 postbuild 步骤卡住了。

5. Build cache 损坏或不匹配

Vercel 在你的 install / build 命令执行前,会先恢复上一次 build 的缓存(node_modules、框架文件)。缓存里有过期或损坏的内容时,build 会反复读到、挂起。缓存 key 由团队、项目、framework preset、根目录、Node.js 版本、包管理器、Git 分支共同推导而来;任何一项变化都会悄悄切换到另一份缓存。

如何判断:同一份代码本地 build 正常、Vercel 卡住;或者卡住是在一次 Node / 包管理器升级之后才开始的。

6. 边缘情况:build 步骤里跑了 watch/dev 命令

vercel.json 或 Build Command 被误写成 next dev / vite --watch,进程永不退出,于是一直显示 Building。

如何判断:log 出现 ready - started server on ...watching for changes——build 命令写错了。

我属于哪一类?

你看到的最后一行 log最可能的成因跳到
Generating static pages (X/Y) 冻住static gen 里的 fetch 没超时Step 2
卡在 install 阶段、没出框架横幅install 网络死锁Step 5(强制干净安装)
出行速度从每分钟几百行掉到 1-2 行OOM / GC 抖动Step 4
build 横幅已完成()但仍 Buildingpostbuild 钩子挂住检查 postbuild
ready - started server / watching for changesbuild 里跑了 dev/watch改 Build Command

最短修复路径

Step 1:取消 build,看 log 停在哪

dashboard → 你的项目 → Deployments → 那条 building 的 deployment → 右上角 菜单 → Cancel Build。然后展开 Deployment Details 区域里的 Building 折叠面板,最后输出的 50-100 行会留在那里,是定位的金矿。

或者用 CLI 拉尾部:

vercel logs <deployment-url> > stuck.log
tail -50 stuck.log

把尾部对照上面的”常见原因”(或那张表)判断卡在哪一阶段。

Step 2:卡在 static generation——给每个外部调用加超时

如果 log 停在 Generating static pages (X/Y),先找出 getStaticProps / getStaticPaths / fetch( 里所有未设超时的外部调用:

// 错误:没有超时,上游一挂整个 build 跟着挂
const data = await fetch(`https://api.example.com/posts/${id}`).then(r => r.json());

// 修复:8 秒超时 + 兜底
try {
  const data = await fetch(url, { signal: AbortSignal.timeout(8000) }).then(r => r.json());
  return { props: { data } };
} catch (e) {
  console.warn(`Skipping ${id}: ${e.message}`);
  return { props: { data: null } };  // 不阻塞 build
}

AbortSignal.timeout(8000)(Node 18+,也就是当前 Vercel 的默认运行时)是过去那套 AbortController + setTimeout 写法的一行简化版。

Step 3:上千个 page——交给 ISR / on-demand

不要在 build 阶段预生成几千个 static page;大页数的串行 SSG 是撞上 45 分钟上限最常见的一条路。Next.js:

// app 或 pages router——build 时不生成,按需填充缓存
export async function getStaticPaths() {
  return {
    paths: [],            // build 时不预生成任何 page
    fallback: 'blocking', // 第一次请求时生成并缓存
  };
}

export async function getStaticProps({ params }) {
  return {
    props: { /* ... */ },
    revalidate: 3600,     // 每小时后台重新生成
  };
}

Astro:保持 output: 'server'(旧版本用 hybrid),并在动态路由上设 export const prerender = false,让它们在请求时渲染而不是在 build 阶段。

Step 4:OOM——加大堆、看 GC、必要时上 enhanced builds

先把容器里已有的 8 GB 多分一些给 Node,并打开 GC 追踪:

// package.json
{
  "scripts": {
    "build": "NODE_OPTIONS='--max-old-space-size=7168 --trace-gc' next build"
  }
}

堆要略低于 8192 MB 的容器上限(7168 给 OS 和子进程留出余量)——直接设成 8192 反而可能触发 OOM kill。--trace-gc 会把每次 GC 耗时打到 log,能直接看出是不是 GC thrashing。

如果 8 GB 确实不够,普通升 Pro 没用——容器在 Hobby 和 Pro 上都是 8 GB。Pro 和 Enterprise 可以在 Settings → Builds 里打开 larger build machines(Enhanced builds 大致是 8 核 CPU、16 GB 内存、58 GB 磁盘),按 build 分钟计费。否则就把重活(类型检查、大规模 codegen)从 build 里挪到单独的 CI 任务。

Step 5:清 build cache,干净重新部署

dashboard → deployment 旁边的 Redeploy,然后取消勾选 Use existing Build Cache,强制全新 install + build。

CLI 等价写法:

vercel --force            # 不带 build cache 部署

或者给项目设环境变量 VERCEL_FORCE_NO_BUILD_CACHE=1,在你删掉它之前每次 build 都跳过缓存。如果清缓存后能过,原因就是缓存;之后在有风险的改动(Node 版本升级、换包管理器)之后主动让 cache key 失效——这些操作本身就会轮换 key。

Step 6:让 build 快速失败,而不是干等 45 分钟

调试期间,用 timeout 包住 build 命令,这样挂住的 build 会在几分钟内带着明确退出码死掉,而不是耗满整个窗口:

# Build Command(Settings → Build & Development,或 vercel.json 的 "buildCommand")
timeout 15m next build

触发时你会在 15 分钟内拿到 Error: Command "timeout 15m next build" exited with 124(124 是 timeout 的标准退出码),于是每一轮调试都很快。build 恢复正常后再改回普通的 next build

怎么确认修好了

  • 部署走到 Ready,而不是又一次”Building → cancelled”。
  • log 里 Generating static pages (X/Y) 跑到结束,总 build 时长回到你平时的基线以下。
  • 如果你在排 OOM,VERCEL_BUILD_SYSTEM_REPORT=1 的报告显示峰值内存稳稳低于 8192 MB,且没有 OOM 事件。
  • 再触发一次带缓存的部署;如果依旧很快,就坐实了”缓存损坏”这个判断并已解决。

预防建议

  • 所有 getStaticProps / getStaticPaths 里的 fetch 强制带 AbortSignal.timeout(8000)
  • 超过约 200 个 page 就改 ISR / on-demand,不在 build 阶段全量生成。
  • CI 里加 build 时长 alert:超过历史均值 1.5 倍就 fail,逼你在撞上 45 分钟上限前先优化。
  • 每个 postbuild script 都加显式 timeout 和干净的 process.exit(0)
  • 在一段”不太稳”的时期里保持 VERCEL_BUILD_SYSTEM_REPORT=1 开着,让 OOM 和磁盘缓增在拖垮 build 之前先暴露出来。
  • 监控 deployment 时长趋势,缓慢上涨往往是技术债的早期信号。

常见问题

卡住的 build 会消耗我的 build minutes 吗? 会。Vercel 按容器运行的整段时间计 build minutes,所以一个挂到 45 分钟上限的 build,和 45 分钟真活计费一样。抓到 log 尾部后就尽早取消。

能把 45 分钟的 build 超时调大吗? 不能——截至 2026 年 6 月,45 分钟是所有套餐的硬性平台上限,没有任何设置可以延长。你只能把 build 改(ISR、减页数、把工作挪到 CI),或者用 Step 6 里的 timeout 15m next build 让它更快失败

本地 build 一切正常,到 Vercel 上就卡住,为什么? 通常三种原因:你代码调用的某个外部 API 从你本机能通、但从 Vercel 的 build 网络被挡(于是没超时的 fetch 挂住);一份本机没有的过期 Vercel build 缓存;或者 Vercel 上更高的真实并发把串行 SSG 的瓶颈暴露了出来。用干净检出复现:rm -rf node_modules .next && npm ci && npm run build,再来一次不带缓存的 vercel --force

怎么区分”卡住”和”只是慢”? 看 log 时间戳。慢的 build 仍然会出新行,只是间隔越拉越大;卡住的 build 是好几分钟一行不出、而转圈还在转。如果 Generating static pages (X/Y) 已经 5 分钟没动,就当卡住处理。

取消 build 会不会弄坏什么? 不会。被取消的 build 从不发布,而且 Vercel 只在 build 成功时才更新缓存,所以取消会把上一份好缓存原样留着。你线上的生产部署也不受影响。

相关阅读

标签: #部署 / 托管 #排查 #排查 #Vercel #构建报错