部署失败,日志里写着 A Serverless Function has exceeded the unzipped maximum size of 250 MB(Vercel)、Generated Pages Functions bundle size is over the limit of 25.0 MiB(Cloudflare Pages),或 The function "XXX" is larger than the 50MB limit / Unzipped size must be smaller than 262144000 bytes(Netlify)。这些都是宿主硬限制,重试再多次也不会过。修复套路始终一样:找出最大的那块、砍掉或挪走它,再在 CI 里把体积守住,不让它悄悄涨回来。
最快的修复(覆盖 90% 情况):某个重量级依赖——通常是 puppeteer、playwright、sharp、canvas、@ffmpeg/core 或某个 TensorFlow 构建——被打进了 serverless function。先跑一遍 bundle visualizer(下面 Step 1),确认元凶,再把它换成 serverless 友好的版本,或者从 function 里挪出去。这一个改动就能解决绝大多数体积超限失败。
各平台当前限制(截至 2026 年 6 月)
限制会变,调 CI 阈值前请先对照各家官方文档核实。以下为 2026 年 6 月对照 Vercel Functions 限制、Cloudflare Workers 限制 和 Cloudflare Pages 限制 文档核实的数据:
| 平台 | 限的是什么 | 上限 |
|---|---|---|
| Vercel Functions(Node.js) | 单 function bundle,解压后 | 250 MB 解压后 / 约 50 MB 压缩(Python 为 500 MB) |
| Vercel Function 请求/响应体 | 单次请求 | 4.5 MB |
| Cloudflare Workers(免费) | Worker 脚本,gzip 后 | 3 MB gzip 后(压缩前 64 MB) |
| Cloudflare Workers(付费) | Worker 脚本,gzip 后 | 10 MB gzip 后(压缩前 64 MB) |
| Cloudflare Pages Functions | 生成的 _worker.js bundle | 压缩后 25 MiB |
| Cloudflare Pages / Workers 静态资源 | 单文件 | 25 MiB |
| Cloudflare 静态资源数量 | 单次部署文件数 | 20,000(免费)/ 100,000(付费) |
| Netlify Functions | 压缩 / 解压 | 50 MB 压缩,250 MB 解压(262144000 字节) |
这里有两个常见误解。第一,Vercel 那个老的”50 MB”指的是压缩包上传上限;你在报错里看到的数字是 250 MB 解压后那个(Python 为 500 MB)。两者都是 AWS Lambda 的限制,都不可调。第二,Cloudflare 把免费 Worker 限制从 1 MB 提到了 3 MB(gzip 后),付费档静态资源数量从 20,000 提到了 100,000——但要在 Pages 上真正拿到 100,000 这个上限,你必须把构建变量 PAGES_WRANGLER_MAJOR_VERSION=4 设上。如果你照着 2024 年的文章在调,这些数字都过时了。
你属于哪一类?
用报错字符串直接定位原因。
| 报错文本包含 | 最可能的原因 | 跳转 |
|---|---|---|
exceeded the unzipped maximum size of 250 MB(Vercel) | Vercel Function 里有个重依赖 | 原因 1、4 |
Unzipped size must be smaller than 262144000 bytes / larger than the 50MB limit(Netlify) | 重依赖,或整个 public/ 被打进了 function | 原因 1、2 |
bundle size is over the limit of 25.0 MiB(Cloudflare Pages) | Pages Function 里有重依赖、source map,或没开 nodejs_compat | 原因 1、5 |
Script startup exceeded CPU time limit / Worker exceeded ... size | Worker bundle 太大(免费 3 MB gzip 后) | 原因 1、5 |
20,000 files / too many files(Cloudflare) | 静态资源太多(去重、清理) | 原因 2 |
| 产物巨大但业务代码很小 | tsconfig 没排除测试/fixture | 原因 3 |
| client chunk 里出现了服务端包名 | server/client 边界没分清 | 原因 4 |
常见原因
按命中率从高到低排。
1. 一个依赖打包后体积惊人
puppeteer(带 Chromium,~170 MB)、playwright、canvas、sharp、@ffmpeg/core、@tensorflow/tfjs-node 这一类原生 / 浏览器引擎依赖是常见元凶。看似只 import 了一个函数,实际把整个二进制都打进了产物。
Vercel 典型报错:
Error: The Serverless Function "api/screenshot" is 287 MB which exceeds
the maximum unzipped size of 250 MB.
如何判断:du -sh node_modules/* | sort -h | tail -20 找出最大的 20 个依赖,对照部署产物。在 Vercel 上,把 VERCEL_ANALYZE_BUILD_OUTPUT=1(或 VERCEL_BUILDER_DEBUG=1)设成构建环境变量,部署日志里就会打印每个 function 的体积明细。
2. 静态资源未压缩或被重复打入产物
原图未压缩(4 MB 的 JPG)、字体文件全 weight 全字符集都打进去(每个 woff2 几百 KB),或者 public/ 和 src/assets/ 同时引用了同一份图片,导致最终 bundle 出现两份。在 Cloudflare 上,这还可能触发文件数量上限(免费档 20,000 个文件),而不是体积上限。
如何判断:ls -lhS dist/ | head 列出最大的静态文件,目测哪些超 500 KB 但其实可以小很多。看文件数用 find dist -type f | wc -l。
3. tsconfig 没排除测试 / 示例 / dev 工具
tsconfig.json 缺 exclude,build 把 *.test.ts、__fixtures__/、storybook/、examples/ 全编译进了 server bundle。表现:明明业务代码不多,产物却几百 MB。
如何判断:build 后 find dist -name "*.test.*" -o -name "*fixture*",能找到就是没排干净。
4. server / client bundle 边界没分清
把 server-only 的依赖(如 pdf-parse、@aws-sdk/client-s3)错误地 import 到了 client component,bundler 把它打到了 browser bundle。或者反过来:本应留在 server 的 markdown / 模板字符串被 inline 到了每个 page。
如何判断:跑 bundle analyzer,看 client chunk 里出现了哪些理应只在服务端的包名。
5. source map 进了生产产物
vite.config.ts / next.config.js 默认有时在 production 也输出 source map,每个 chunk 旁边都跟一份同体积的 .map 文件,直接翻倍产物大小——这是 Cloudflare Pages 25 MiB 和 Worker 3 MB/10 MB 超限的常见原因。
如何判断:ls dist/assets/*.map 2>/dev/null | wc -l 大于 0,且 .map 文件总大小接近代码总大小。
最短修复路径
Step 1:定位”谁最大”
先看产物物理体积:
npm run build
du -sh dist/
du -sh dist/* | sort -h | tail -10
再跑 bundle visualizer 找出每个 chunk 里的依赖构成:
# Vite / Astro
npx vite-bundle-visualizer
# Next.js(需要先装 @next/bundle-analyzer 并在 next.config.js 里配置)
ANALYZE=true npm run build
# Webpack
npx webpack-bundle-analyzer dist/stats.json
打开生成的 HTML,按面积找出占比最大的 1-3 个依赖,那就是要砍的目标。
Step 2:把大依赖换轻量版本或推到 server-only
| 重 | 轻 |
|---|---|
moment (~290 KB) | date-fns (~13 KB tree-shaken) 或 dayjs (~7 KB) |
lodash (~70 KB) | lodash-es 按需 import,或原生 ES 方法 |
axios (~30 KB) | 原生 fetch |
puppeteer | @sparticuz/chromium + puppeteer-core(serverless 专用) |
完整 chart.js | 按需 import 用到的模块 |
如果依赖必须用且必须重(比如 sharp、puppeteer),把它从 serverless function 拆出来:迁到独立 worker、用外部服务(Browserless、Cloudinary),或者改 build-time 处理。在 Cloudflare 上要特别注意,puppeteer、playwright 根本塞不进一个 Worker——请改用 Cloudflare Browser Rendering,而不是把浏览器引擎打进 bundle。
在 Cloudflare Pages 上,动手砍之前先确认 nodejs_compat 兼容性标志已开启(Settings -> Functions -> Compatibility flags,或在 wrangler.toml 里写 compatibility_flags = ["nodejs_compat"])。开了它,Cloudflare 会用内置的 Node 兼容 API,而不是把一堆 polyfill 打进来,光这一项就常能把 Next.js 的 _worker.js bundle 拉回 25 MiB 线以下。另一个在 Pages 上很管用的招是按需 import 子包(用 @googleapis/calendar 而不是整个 googleapis)。
Step 3:收紧 tsconfig 和 build include / exclude
// tsconfig.json
{
"compilerOptions": { "...": "..." },
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts",
"**/__tests__/**",
"**/__fixtures__/**",
"examples/**",
"storybook/**"
]
}
在 Vercel 上,vercel.json 可以按 function 精细排除:
{
"functions": {
"api/**/*.ts": {
"includeFiles": "lib/**",
"excludeFiles": "{tests,fixtures}/**"
}
}
}
注意:Next.js 会忽略 vercel.json 里的 includeFiles / excludeFiles。Next.js 项目请改用 next.config.js 里的 outputFileTracingIncludes / outputFileTracingExcludes:
// next.config.js
module.exports = {
outputFileTracingExcludes: {
"*": ["**/*.test.*", "**/__fixtures__/**", "node_modules/@swc/**"],
},
};
Step 4:压缩静态资源 + 关 source map
图片:
# 一次性把 public/ 下所有 PNG / JPG 压缩
npx @squoosh/cli --mozjpeg auto public/**/*.{jpg,jpeg}
npx @squoosh/cli --oxipng auto public/**/*.png
# 或者全部转 WebP / AVIF
npx @squoosh/cli --webp auto public/**/*.{jpg,png}
关掉 production source map:
// vite.config.ts
export default defineConfig({
build: {
sourcemap: false, // 或 'hidden' 上传到 Sentry 后不发布
},
});
Step 5:在 CI 里加 bundle-size 守门
# .github/workflows/bundle-size.yml
name: Bundle Size
on: [pull_request]
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci && npm run build
- name: Check size
run: |
SIZE=$(du -sm dist | cut -f1)
echo "dist size: ${SIZE} MB"
if [ "$SIZE" -gt 180 ]; then
echo "Bundle exceeds 180 MB threshold"
exit 1
fi
阈值设在平台硬限制的 70-80%,给未来增长留 buffer(比如对 Vercel 的 250 MB 设 ~180 MB,对 Cloudflare 付费 Worker 的 10 MB 设 ~7 MB)。
如何确认修好了
- 本地重跑 build 对比:
du -sh dist/应该明显比之前小。 - Vercel 上把
VERCEL_ANALYZE_BUILD_OUTPUT=1打开,在部署日志里看每个 function 的体积——每个都应该明显低于250 MB。 - 重新部署。日志里没有体积超限报错的干净部署才是真正的确认;构建日志末尾会列出最终的 function / 资源体积。
- 验证 CI 守门有效:临时把阈值调到低于当前体积,看 PR 检查变红,再调回来。
预防建议
- 每加一个新依赖前,先去 bundlephobia.com 查它 minified + gzipped 后的体积,超
50 KB的要给出理由或找替代。 - CI 里的 bundle-size 检查阈值固定在平台硬限制的 70-80%。
- 区分 server-only 和 client-only 依赖,server 包别 import 到客户端组件。
- 大型二进制(PDF / 视频 / 模型权重)放对象存储(R2、S3、Cloudinary),运行时拉取,不打进 bundle。
- 每季度跑一次
npx depcheck删未使用依赖,再跑一遍 bundle visualizer 复盘 top 5 大依赖是否还都需要。
常见问题
有的 Vercel 文档说限制是 50 MB,可我的报错写的是 250 MB,到底是哪个?
两个都对。AWS Lambda(Vercel 和 Netlify 的 Functions 底层都是它)把压缩包上传限制在约 50 MB、解压后限制在 250 MB。Vercel 在构建报错里给你看的是 250 MB 解压后那个。截至 2026 年 6 月,把”解压后 250 MB(Python 为 500 MB)“当成你要守住的上限就对了;这是 AWS 强制的、不可调。可参考 Vercel 的 function 限制页。
我的 Cloudflare Worker 去年还好好的,现在却”超限”了,是限制缩水了吗?
不是——Cloudflare 反而把免费 Worker 限制从 1 MB 提到了 3 MB(gzip 后;付费档是 10 MB,两档压缩前都还有 64 MB 上限)。如果你是新近才超的,说明某个依赖变大了,或者你开始打进了什么东西(常见的是 source map 或新装的包)。用 wrangler deploy --dry-run --outdir dist 在部署前先看压缩后的体积,Pages 上则先确认 nodejs_compat 是开着的。
压缩包才 30 MB,Netlify 却说超过 50 MB(或 262144000 bytes),为什么?
Netlify 拿解压后体积去比 250 MB(262144000 字节),拿压缩后体积去比 50 MB。一个 30 MB 的 zip 如果里面是浏览器引擎或原生二进制,解压后完全可能远超 250 MB。部署前先在本地看解压后的体积。
删掉部署再重试有用吗? 没用。体积限制是确定性的——同一份源码每次都产出同样超大的 bundle。你必须真的把那块重量删掉或挪走(Step 2-4)。重试只会白白消耗构建分钟数。
怎么在不爆体积的前提下保留浏览器自动化依赖?
别把浏览器打进去。Vercel 上用 puppeteer-core + @sparticuz/chromium;Cloudflare 上用 Browser Rendering;或者交给 Browserless 这类托管服务。这样 function 里只带客户端库,不带那 ~170 MB 的引擎。