上周还 8 分钟跑完的 Vercel 构建,今天跑了 45 分钟被强杀,日志里写着 Build exceeded maximum duration of 45m。本地 next build 或 astro build 4 分钟跑完。原因几乎不是代码一夜变慢,而是:构建缓存被回收导致全冷安装、getStaticPaths 意外返回了远超预期的页面数、content 扫描走进了奇怪的 node_modules,或者某个 post-build 钩子(sitemap、RSS、搜索索引)卡在网络调用上。
常见原因
按实际出现频率排序。
1. 构建缓存被回收,整次冷装
Vercel 构建缓存目录在 .vercel/cache/,跨部署保留 node_modules、.next/cache 和各框架缓存。项目闲置约 7 天,缓存会被回收,下一次构建就是冷装 + 冷框架缓存,耗时可能翻 4-6 倍。
如何识别:正常构建日志开头是 Restored build cache from ...,慢的那次显示 No build cache found 或 Build cache miss。
2. getStaticPaths / content collection 返回的页面数失控
像 paths: posts.flatMap(p => tags.map(t => p)) 这种写错(应该按 tag 映射)会让生成的页面数翻 10-100 倍。每页几百毫秒,5 万页就能把 45 分钟吃完。
如何识别:日志显示 Generating static pages (47832/250000),和上次绿色构建的页面数一对比立刻露馅。
3. 构建后脚本卡在网络请求上
sitemap、RSS、OG 图、搜索索引同步调用外部 API 时,可能按 HTTP 超时(默认 120 秒)卡住每一项。几百项叠加就爆预算。
如何识别:next build / astro build 部分 5 分钟跑完,但 node scripts/build-sitemap.mjs 之类的脚本卡在日志里不再输出。
4. 内存压力导致末尾 GC 抖动
构建进程堆耗尽进入 V8 慢 GC 模式后,最后 20% 的页面可能比前 80% 慢 10 倍。最后要么 OOM 要么超时。
如何识别:每批页面耗时明显变慢(前 1000 页 2 分钟,下一个 1000 页 8 分钟),或者出现 FATAL ERROR: ... heap out of memory。
5. 大型依赖安装(puppeteer、sharp-libvips、Playwright)
每次冷构建都下载 300 MB Chromium 的 postinstall,单独就能加 5-10 分钟。叠加冷缓存就把构建挤爆。
如何识别:安装日志里出现 Downloading Chromium ... 或 Downloading libvips ...,单步耗时比整个 install 的剩余部分都长。
6. monorepo 全图 tsc 类型检查
tsc --noEmit 跨 200 个包却没启用 project references / incremental,每次都把所有文件走一遍。冷缓存下单这一步能耗 10-20 分钟。
如何识别:日志里有一段 tsc 调用,5 分钟以上不输出任何内容才进入下一步。
7. 体量过大的 bundle 在压缩阶段卡住
如果某个客户端 bundle 涨到 50 MB 以上(常见是某处动态 import 误带进 aws-sdk v2 或 mongodb),Terser/SWC minify 会变成原来 10 倍耗时。
如何识别:日志 Optimizing bundle... 或 Minifying... 卡了好几分钟。比对上次绿色构建的 bundle 体积。
开始排查前
- 抓完整的失败构建日志,Vercel 在 Deployments → Build Logs 里。
- 弄清楚是被卡 45 分钟强杀、OOM 杀、还是某行后再无输出。
- 对比上次绿色构建的耗时和页面数,与失败构建最后一条页面计数比对。
- 本地
vercel build --prod必须能跑完,才有和 CI 做 A/B 的基础。
需要收集的信息
- 构建开始/结束时间戳和最后一条成功日志。
next.config.js/astro.config.mjs,确认 ISR/SSG/output: static配置。- 健康构建与失败构建的页面生成数对比。
package.json的scripts.build以及postbuild钩子。- 本地
du -sh node_modules/的冷装大小。 - 是否使用 Turbo / Nx / Lerna,构建图什么样。
分步修复
按性价比排序。
步骤 1:禁用缓存重跑,定位冷启动还是构建本身
Vercel 面板里 Deployments → … → Redeploy → 取消勾选 “Use existing build cache”。如果冷构建依然 45 分钟,问题在构建本身;如果只在首次冷构建出现,那就是缓存回收。
步骤 2:构建前先打印页面数
在 prebuild 阶段加一个断言:
// scripts/check-page-count.mjs
import { glob } from "glob";
const files = await glob("src/content/**/*.{md,mdx}");
console.log(`[precheck] content files: ${files.length}`);
if (files.length > 10000) {
console.error("[precheck] page count over threshold");
process.exit(1);
}
配成 prebuild 钩子。源数据爆炸时直接快速失败。
步骤 3:把 post-build 脚本用超时包起来
任何带网络调用的构建后步骤都该有硬超时:
# package.json scripts.postbuild
"postbuild": "timeout 300 node scripts/build-sitemap.mjs || echo 'sitemap step skipped'"
5 分钟到点直接干掉,构建继续。下次部署再生成 sitemap。一份过期 sitemap 也比被强杀强。
步骤 4:monorepo 切到 Turborepo 任务图
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"],
"cache": true
}
}
}
根目录跑 turbo run build --concurrency=4 取代 npm run build。包之间命中缓存可省 60-80%。
步骤 5:重型二进制不要在构建期下载
Chromium / Playwright 不要在 Vercel 安装阶段下载。函数里改用 @sparticuz/chromium,或者截图任务挪到独立 worker(Render、Fly、AWS Lambda 层),构建只 fetch URL。
# package.json
"postinstall": "echo 'skipping chromium download in build'",
Vercel 环境变量里设 PUPPETEER_SKIP_DOWNLOAD=true。
步骤 6:大量 SSG 页面切到 ISR / 按需生成
如果 getStaticPaths 返回 5 万多页而多数页面访问稀疏:
// pages/posts/[slug].tsx
export async function getStaticPaths() {
const topPosts = await fetchTopPosts(500); // 构建期只生成前 500 页
return {
paths: topPosts.map(p => ({ params: { slug: p.slug } })),
fallback: "blocking", // 其余首次请求时生成
};
}
构建时间随预生成集线性下降。运行期表现见 Next.js ISR revalidation stuck。
步骤 7:调大 Node 堆并二分找瓶颈
# vercel.json 或环境变量
NODE_OPTIONS="--max-old-space-size=8192"
然后二分:临时注释掉所有 post-build 脚本,看框架自己 build 多久;差值就是 post-build 的代价,再逐个回插定位。
验证
- 构建时长回到上次绿色构建的 1.5 倍以内。
- 修复后下一次构建日志显示
Restored build cache。 - 页面生成数符合预期,不再出现莫名的 10 倍。
- 每个 post-build 脚本都有清晰的开始/结束行。
- 用
--force不带缓存测试一次,依然能在 45 分钟内跑完。
长期预防
- 构建时长报警:任何部署超过滚动平均 1.5 倍就触发通知。
- 所有
postinstall重下载都用环境变量 gate,CI 上可跳过。 - 每次构建末尾打一行
[build-stats] pages=N duration=Xs bundle-size=Y方便日后 grep 趋势。 - 把缓存 miss 视为一种已知代价,对闲置项目每周跑一次预热部署。
- 包数超过 3 的 monorepo 必须上
turbo或nx的输出缓存。 getStaticPaths只预生成 Top-N 热门,长尾交给 ISR / 按需。
常见坑
- 升级到 Enterprise 想”拉高上限” —— Vercel 45 分钟上限对所有套餐都一样,要的是真修复不是换账单。
- 给已经 OOM 抖动的构建加并发 —— 只会更糟。
- 拼命缓存
node_modules却忘了.next/cache—— 真正提速的部分在那里。 - 一遇问题就”清缓存重跑”,把每次冷启动 4-6 倍的代价吃满。
- 给卡死的网络调用加
sleep 60重试而不是硬超时,相关 hang 模式见 Vercel stuck building。
常见问答
Q: 我是 Pro,能不能把构建上限提到 60 分钟?
不行,45 分钟是 Hobby/Pro/Enterprise 通用平台上限。超过就要拆 monorepo、ISR fallback,或者搬到 GitHub Actions 预构建产物。
Q: 缓存恢复了但构建还是慢。
缓存恢复只对写了缓存目录的框架有效。确认 .next/cache、.astro/、node_modules/.cache/ 没有被某个 clean 步骤删掉,也看下 vercel.json 的 buildCommand 里有没有 rm -rf。
Q: 要不要把构建搬到 GitHub Actions,只把产物推给 Vercel?
如果构建稳定超过 30 分钟,建议这么做。CI runner 上跑 vercel deploy --prebuilt,标准 Vercel 端流程参考 Vercel build failed。
Q: 能看到缓存为什么被回收吗?
不能,Vercel 不暴露缓存 LRU 事件。经验上:项目闲置 7 天以上、构建命令变更、Node 主版本变更都会让缓存失效,提前心里有数即可。