Vercel 部署列表里那条 Building 状态已经转了 15 分钟还没动,log 也不出新行——这种”卡住”和”build failed”是两类问题。Failed 会显式抛错并结束,卡住则是进程没死但没进展。Vercel 的默认 build 超时是 45 分钟(Hobby)或 60 分钟(Pro),超时后才显式终止;在这之前你看到的卡住,常见是 install 阶段死循环、static generation 卡某个 page、postbuild 钩子调用阻塞 API 没返回、或单一 worker 在做几千页 SSG。本文给出按命中率排的诊断顺序和具体超时配置。
常见原因
按命中率从高到低:
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 分钟]
如何判断:log 显示 Generating static pages (X/Y) 不再增长 > 5 分钟,几乎可以确定。
2. npm install 阶段网络死锁
npm install 试图拉某个 registry 不可达的包(私有 registry、被墙的 CDN、404 的 git URL),重试到超时。log 卡在 Installing dependencies... 后面没东西。
npm warn deprecated ...
[十几分钟没新行]
如何判断:log 最后一段是 install 阶段,没看到 Build completed in installation step 或框架名出现。
3. Build OOM——内存压满但没崩
不是 exited with 137 那种立刻死,而是堆内存压到 99% 后 GC 反复运行,进程没死但每步慢到接近停滞。常见于 TypeScript 项目大规模类型推导、webpack chunk 拆分、Turborepo 并发任务。
如何判断:log 行的时间戳之间间隔越来越长(前 5 分钟出几百行,之后每分钟出 1-2 行),多半 GC thrashing。
4. Postbuild hook 调用外部服务阻塞
package.json 里有 "postbuild": "node scripts/notify-cdn.js",脚本里 fetch 一个 webhook 没设超时;或者 sitemap-generator 跑全站爬虫卡在某条 URL。
$ next build && next-sitemap
✓ Generating static pages (1000/1000)
$ next-sitemap
[卡这]
如何判断:log 显示 build 主流程已完成(出现 ✓ 或 Build completed),但状态还是 Building——一定是 post 步骤卡住。
5. Build cache 损坏
Vercel 默认复用上次 build 的 .next/cache 或 node_modules,缓存里有坏文件时 build 反复读到出错或挂起。
如何判断:同一份代码本地 build 正常,Vercel 卡;去 dashboard 的 deployment 看上一次失败的 build cache 时间戳是否非常旧。
6. 边缘情况:开了 --watch 模式
vercel.json 或 build command 误写成 next dev / vite --watch,进程不会退出,永远显示 building。
如何判断:log 出现 ready - started server on ... 或 watching for changes——build 命令写错了。
最短修复路径
Step 1:先在 dashboard 手动取消,看 log 停在哪
dashboard → 那条 building 的 deployment → 右上角 ... → Cancel Build。取消后 log 会留下最后输出的 50-100 行,这是定位的金矿。
把最后 30 行整段复制下来:
vercel logs <deployment-url> > stuck.log
tail -50 stuck.log
按上面”常见原因”对照判断在哪一阶段。
Step 2:static generation 卡住——给外部调用加超时
如果是 Generating static pages (X/Y) 卡住,先 grep getStaticProps / getStaticPaths / fetch( 里所有未设超时的外部调用:
// 错误:没有超时
const data = await fetch(`https://api.example.com/posts/${id}`).then(r => r.json());
// 修复:8 秒强制超时 + 兜底
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const data = await fetch(url, { signal: controller.signal }).then(r => r.json());
return { props: { data } };
} catch (e) {
console.warn(`Skipping ${id}: ${e.message}`);
return { props: { data: null } }; // 不阻塞 build
} finally {
clearTimeout(timeout);
}
Step 3:千页 SSG 改 ISR / On-Demand
build 阶段不要生成几千个 static page。Next.js:
// pages/[slug].tsx
export async function getStaticPaths() {
return {
paths: [], // build 时不预生成任何 page
fallback: 'blocking', // 第一次请求时生成并缓存
};
}
export async function getStaticProps({ params }) {
return {
props: { ... },
revalidate: 3600, // 1 小时后台重新生成
};
}
Astro:用 output: 'hybrid' + 动态路由 prerender = false。
Step 4:增大 build 内存 + 看 GC 状态
# package.json
{
"scripts": {
"build": "NODE_OPTIONS='--max-old-space-size=8192 --trace-gc' next build"
}
}
--trace-gc 会在 log 打印每次 GC 耗时,能直接看出是不是 GC thrashing。Hobby 上限就 8GB,再大要升 Pro。
Step 5:清 build cache 重新部署
dashboard → deployment 旁边的 ... → Redeploy,取消勾选 Use existing Build Cache,强制全新 install + build。
或 CLI:
vercel --force
如果清缓存后能过,那原因就是缓存损坏;之后给 build cache 加版本号(改 package.json 里加一个无意义字段触发 cache key 变化)。
预防建议
- 所有
getStaticProps/getStaticPaths里的 fetch 强制带AbortSignal.timeout(8000) - 超过 200 个 page 时改 ISR / on-demand,不在 build 阶段全量生成
- CI 里加 build 时长 alert:超过历史均值 1.5 倍就 fail,逼你优化
- Postbuild script 全部加 timeout + 显式
process.exit(0) - 监控 deployment 时长趋势,缓慢上涨往往是技术债的早期信号
- 定期手动清一次 build cache(每月),避免长期累积坏文件