上周还 8 分钟跑完的 Vercel 构建,今天跑了 45 分钟被强杀,日志里写着 Build step did not complete within the maximum of 45 minutes。本地 next build 或 astro build 4 分钟就跑完。这条 45 分钟的硬上限在 Hobby、Pro、Enterprise 上完全一样(截至 2026 年 6 月),所以升级套餐没有任何用 —— 但在付费套餐上,你可以把构建放到更快的机器上,这往往才是最快的真修复。
最快的判断路径:打开失败构建的日志,看被强杀前的最后一行。如果卡在 Generating static pages (...) 里,那是页面数爆炸(原因 2)。如果卡在某个 postbuild 脚本里、之后再无任何进度行,那是构建后网络请求卡死(原因 3)。如果页面生成是一批比一批慢,那是堆内存耗尽(原因 4)。如果是闲置一周后第一次冷部署就失败,那是构建缓存被回收(原因 1)。对上症状,直接跳到对应的修复。
你属于哪一类?
| 构建日志里的症状 | 最可能的原因 | 跳到 |
|---|---|---|
顶部出现 No build cache found;只在闲置几天后第一次部署才慢 | 缓存被回收,整次冷装 | 步骤 1 |
Generating static pages (47832/250000) —— 页面数远超平时 | getStaticPaths / content 集失控 | 步骤 2、步骤 6 |
某个 postbuild 脚本不再输出、永不返回 | 构建后钩子里的网络请求卡死 | 步骤 3 |
每批页面都比上一批慢;出现 heap out of memory | 内存压力 / GC 抖动 | 步骤 7 |
Downloading Chromium... / Downloading libvips... 占满安装阶段 | postinstall 重型二进制下载 | 步骤 5 |
tsc 5 分钟以上不输出任何内容 | monorepo 全图类型检查 | 步骤 4 |
Optimizing bundle... / Minifying... 卡好几分钟 | 体量过大的客户端 bundle | 步骤 7 |
常见原因
按实际出现频率排序。
1. 构建缓存被回收,整次冷装
Vercel 构建缓存跨部署保留 node_modules/** 以及框架自己的缓存目录(.next/cache、.astro/ 等)。截至 2026 年 6 月,每个项目缓存上限 1 GB,保留一个月。缓存 key 由你的账号/团队、项目、Framework Preset、Root Directory、Node.js 版本、包管理器、Git 分支共同推导得出 —— 其中任何一项变化(Node 主版本升级、换包管理器、新建一个没有生产缓存可回退的分支)都会让 key 变化,于是下一次构建就是冷装 + 冷框架缓存,耗时可能翻 4-6 倍。
如何识别:快的那次日志开头会恢复上一次的缓存;慢的那次显示 No build cache found、什么都没恢复。没有父级生产缓存的新建分支是经典触发场景。
2. getStaticPaths / content collection 返回的页面数失控
像 paths: posts.flatMap(p => tags.map(t => p)) 这种写错(本该按 tag 映射)会让生成的页面数翻 10-100 倍。每页几百毫秒,5 万页就能把 45 分钟吃完。Vercel 官方的说法是:一旦输出文件超过约 10 万个,构建时间就会陡增。
如何识别:日志显示 Generating static pages (47832/250000),和上次绿色构建的页面数一对比立刻露馅。
3. 构建后脚本卡在网络请求上
sitemap、RSS、OG 图、搜索索引同步调用外部 API 时,可能按 HTTP 超时卡住每一项。几百项叠加就爆预算。
如何识别:next build / astro build 部分 5 分钟跑完,但 node scripts/build-sitemap.mjs 之类的脚本卡在日志里不再输出。
4. 内存压力导致末尾 GC 抖动
构建容器在 Standard 机型上有固定 8192 MB 内存(Hobby 和 Pro 的默认机型)。如果构建进程把堆耗尽、掉进 V8 的慢 GC 模式,最后 20% 的页面可能比前 80% 慢 10 倍;最终要么因超内存被取消,要么撞上 45 分钟墙。
如何识别:每批页面耗时明显变慢(前 1000 页 2 分钟,下一个 1000 页 8 分钟),或者出现 FATAL ERROR: ... heap out of memory,或者构建因超内存被取消。
5. 大型依赖安装(puppeteer、sharp-libvips、Playwright)
每次冷构建都下载 300 MB Chromium 的 postinstall,单独就能加好几分钟。叠加冷缓存就把构建挤爆。
如何识别:安装日志里出现 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 会比平时慢得多。
如何识别:日志 Optimizing bundle... 或 Minifying... 卡了好几分钟。比对上次绿色构建的 bundle 体积。
开始排查前
- 抓完整的失败构建日志:项目 → Deployments → 那个报错的部署 → 展开 Building 折叠区。
- 看一下面板侧边栏的 Observability → Build Diagnostics,它会画出你历次构建时长的趋势,能精确看出退化是从哪次开始的。
- 弄清楚失败模式:是被卡 45 分钟硬杀、因超内存/超磁盘被取消、还是某行之后再无输出的卡死?
- 对比上次绿色构建的耗时和页面数,与失败构建最后一条页面计数比对。
- 本地
vercel build --prod必须能跑完,才有和 CI 做 A/B 的基础。
需要收集的信息
- 构建开始/结束时间戳和最后一条成功日志。
next.config.js/astro.config.mjs,确认 ISR/SSG/output: static配置。- 健康构建与失败构建的页面生成数对比。
package.json的scripts.build以及任何postbuild/postinstall钩子。- 设环境变量
VERCEL_BUILD_SYSTEM_REPORT=1强制每次构建都生成系统报告。它会拆出源码、node_modules、输出各自的磁盘占用,标出任何超过 100 MB 的文件,并暴露隐藏的内存溢出事件 —— 对原因 4、5、7 极其有用。 - 是否使用 Turbo / Nx / Lerna,构建图什么样。
分步修复
按性价比排序。
步骤 1:禁用缓存重跑,定位冷启动还是构建本身
面板里 Deployments → 部署右侧三个点 → Redeploy → 在弹窗里取消勾选 Use existing Build Cache。(等价做法:CLI 用 vercel --force,或设环境变量 VERCEL_FORCE_NO_BUILD_CACHE=1。)如果冷构建依然 45 分钟,问题在构建本身,不是缓存回收;如果只有首次冷构建慢、热构建正常,那就是缓存回收 —— 直接看下面的预防部分。
步骤 2:构建前先打印页面数
在 prebuild 阶段加一个断言,源数据爆炸时就直接快速失败,而不是白烧 45 分钟:
// 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);
}
在 package.json 里配成 prebuild 钩子。
步骤 3:给 post-build 脚本加硬超时
任何带网络调用的构建后步骤都用 timeout 命令包起来,卡死的请求就拖不垮整次构建:
# package.json scripts.postbuild
"postbuild": "timeout 300 node scripts/build-sitemap.mjs || echo 'sitemap step skipped'"
timeout 300 在 5 分钟到点直接干掉这一步;|| 让构建继续,下次部署再生成 sitemap。一份过期 sitemap 也比被强杀强。同样的招也能套在整个构建上 —— timeout 40m next build 超时后退出码是 124,日志会打印 Error: Command "timeout 40m next build" exited with 124,这样你拿到的是一次日志完整的干净失败,而不是 45 分钟那记不透明的平台强杀。
步骤 4:把 monorepo 切成带缓存的 Turborepo 任务图
如果是 monorepo,给每个包的输出做缓存,没改动的包就跳过重建,同时限制并发避免 OOM:
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"],
"cache": true
}
}
}
然后根目录跑 turbo run build --concurrency=4 取代 npm run build。包之间命中缓存可省 60-80%。对全图 tsc,启用 TypeScript project references 和 tsc -b --incremental,只让改动的包重新类型检查 —— 或者干脆把类型检查挪出构建(见步骤 8)。
步骤 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(Playwright 用 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1)。
步骤 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 堆(别越过容器上限)再二分找瓶颈
Standard 构建容器有固定 8192 MB 上限,把堆直接设成满的 8192 会让 V8 一点余量都没有,反而可能让 OOM 更严重。在 Standard 机型上,7168 是更稳的封顶值:
# vercel.json 或环境变量
NODE_OPTIONS="--max-old-space-size=7168"
然后二分:临时注释掉所有 post-build 脚本,量一下框架自己 build 多久;差值就是 post-build 的代价,再逐个回插定位。如果框架自身的构建确实需要超过 8 GB,堆参数救不了你 —— 升级构建机器(步骤 8)。
步骤 8:把构建放到更快的机器,或干脆搬出 Vercel
对确实很重的构建,有两个逃生口:
- 更大的构建机器(Pro/Enterprise)。 截至 2026 年 6 月,Vercel 在 Settings → Build and Deployment → Build Machines 里提供更大的构建机型:Enhanced(8 vCPU / 16 GB / 64 GB 磁盘)和 Turbo(30 vCPU / 60 GB),还有在两者之间自动伸缩的 Elastic(新付费团队的默认)。把内存翻到 16 GB 能解决大多数 OOM 相关的卡顿;更多 vCPU 能加速打包和类型检查。这些按 CPU 分钟计费(Elastic 起步约 $0.0035/CPU 分钟),所以是花钱的杠杆而非免费的 —— 但它是唯一真正改变硬件的旋钮。45 分钟的墙依然存在。
- 在 CI 里构建,只部署预构建产物。 对稳定超过 30 分钟的构建,在 GitHub Actions runner 上构建(时间更多、内存更大、没有 45 分钟墙),再用
vercel deploy --prebuilt推产物。把 lint、测试、tsc也都挪进那个 CI 任务,它们就不再占用 Vercel 构建预算。
如何确认已修复
- 构建时长回到上次已知良好构建的 1.5 倍以内。
- 修复后第二次构建(热构建)能恢复上一次的缓存。
- 页面生成数符合预期,不再出现莫名的 10 倍。
- 每个 post-build 脚本都有清晰的开始/结束行。
- 一次强制无缓存部署(
vercel --force)也能在 45 分钟内远远跑完 —— 证明冷路径本身健康,而不只是热路径。
长期预防
- 构建时长报警:任何部署超过滚动平均 1.5 倍就触发通知。面板里的 Build Diagnostics 就是现成的数据源。
- 所有重型
postinstall下载都用环境变量 gate,CI 上可跳过。 - 每次构建末尾打一行
[build-stats] pages=N duration=Xs bundle-size=Y,方便日后 grep 趋势。 - 把缓存 miss 视为一种已知代价。因为缓存闲置一个月会被回收(Node/包管理器/分支 key 变化也会失效),对闲置项目每周跑一次预热部署。
- 包数超过 3 的 monorepo 必须上
turbo或nx的输出缓存。 getStaticPaths只预生成 Top-N 热门,长尾交给 ISR / 按需。
常见坑
- 升级到 Enterprise 想”拉高时间上限” —— 45 分钟墙在 Enterprise 上一模一样。付费套餐上真正有用的是更快的构建机器(步骤 8),不是更高的套餐档。
- 在 Standard 8 GB 容器上设
--max-old-space-size=8192—— 让 V8 一点余量都没有,反而会触发你本想避免的 OOM。 - 给已经 OOM 抖动的构建加并发 —— 只会更糟。
- 拼命缓存
node_modules却忘了.next/cache—— 真正提速的部分在那里。 - 一遇问题就”清缓存重跑”,把每次冷启动 4-6 倍的代价吃满。
- 给卡死的网络调用加
sleep 60重试,而不是用硬timeout。相关 hang 模式见 Vercel stuck building。
常见问答
Q: 我是 Pro,能不能把构建上限提到 45 分钟以上?
不行。45 分钟是 Hobby、Pro、Enterprise 通用的平台上限(2026 年 6 月)。你能做的是让构建更快跑完(更大的构建机器,步骤 8),但提不高这堵墙。确实需要超过 45 分钟的构建只能拆 —— 拆 monorepo、ISR fallback,或者用 CI 预构建后推 --prebuilt 产物。
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 事件,但规则是确定的。缓存保留一个月,闲置后被回收。缓存 key 变化时也会失效,而这个 key 由账号/团队、项目、Framework Preset、Root Directory、Node.js 版本、包管理器、Git 分支共同推导。所以 Node 主版本升级、npm↔pnpm 互换、或在一个没有父级生产缓存的全新分支上构建,都会强制冷构建。提前心里有数即可。
Q: 构建缓存能有多大?能控制缓存哪些文件吗?
每个项目的构建缓存上限 1 GB,且无法手动配置缓存哪些文件 —— Vercel 根据 Framework Preset 缓存 node_modules/** 以及框架已知的缓存目录。如果缓存逼近 1 GB,就清理过大的产物(用 VERCEL_BUILD_SYSTEM_REPORT=1 看是什么在吃磁盘),让缓存保持有用。