Vite Monorepo 项目部署到 Vercel 的构建避坑实践指南

发布时间:2026/10/8 4:08:41
Vite Monorepo 项目部署到 Vercel 的构建避坑实践指南 看着本地跑得好好的 Vite 项目推到 Git 仓库后却眼睁睁看着 Vercel 构建失败这种经历我相信每个用过 Vite Monorepo 的人都懂。更糟心的是明明昨天还能构建今天因为 Node 版本、依赖缓存或者路径引用的某个小改动又莫名其妙挂了。我过去小半年把好几个 Vite 项目从单仓库拆到 Monorepo再统一部署到 Vercel前前后后踩了不下二十个坑。这篇文章不打算写成文档翻译只记录那些我在实践中反复踩、翻源码、看 issue 才搞清楚的东西以及背后的原理。如果你正准备把 Vite Monorepo 项目搬到 Vercel或者已经在搬的路上但被构建日志折磨得头疼这篇文章应该能帮你省下不少时间。1. Vercel 的构建运行环境到底是怎么运作的开始讲具体配置之前先得把 Vercel 的构建机制说明白。很多人拿着 Vercel 当云端电脑用其实从工程角度讲它更像一个高度定制的 CI/CD 流水线。理解它的行为模式排错才能有的放矢。1.1 构建沙箱与路径规则Vercel 的每一次部署都会启动一个全新的干净环境然后执行一系列预定义 Hook。大致流程是这样的拉取代码、安装依赖、执行构建命令、收集产物、部署到边缘网络。这里有几个关键规则几乎决定了 Monorepo 能不能在 Vercel 上顺利跑起来根目录Root Directory规则Vercel 默认认为你的仓库根目录就是项目根目录。对于 Monorepo情况完全不同——你的 Vite 应用可能放在packages/web里构建命令工作目录应该在这里而不是仓库根目录。没有指定 Root DirectoryVercel 会尝试在仓库根目录找package.json要是没找到或者找错了构建会直接失败。我在很多项目里看到过这样的配置失误明明 app 在apps/admin-web下Vercel 设置里却把 Root Directory 留空了结果构建时报Could not find package.json一脸懵。注意Root Directory 一旦设定package.json、pnpm-workspace.yaml、turbo.json这些文件的相对路径解析都会基于这个目录重新定位这一点要格外留意。Node 版本差异性Vercel 默认使用的 Node 版本可能会滞后于你本地版本。Vite 6 之后对 Node 版本要求明显提高需要 18 或 20如果你的本地环境是 Node 20 但 Vercel 默认环境是 16构建时可能直接报语法错误或者原生模块加载失败。设置里务必将 Node 版本显式指到项目实际使用的版本。1.2 构建系统默认查找逻辑Vercel 会自动探测项目类型。它会读取根目录的package.json然后决定使用什么构建系统。对于纯 Vite 项目它通常能自动识别出vite build命令。但问题来了——Monorepo 里这个自动识别经常给你错误答案。比如你用 pnpm workspace 管理多个包Vercel 可能会把根目录当成一个应用去构建然后跟着buildscript 的设置去执行。根目录没有 build script 时它会尝试默认的vite build结果是传统vite命令找不到报 vite: not found。原因很简单——Vitest、Vite 这些依赖都装在工作区的子包里根目录的 node_modules 里没有。解决方式是显式告诉 VercelRoot Directory 填apps/webBuild Command 填pnpm buildInstall Command 填pnpm install。少一个它就可能猜错。2. Monorepo 部署前的基础设施准备没有好的地基部署再多都是徒劳。Monorepo 能顺利上 Vercel前提是本地工程化足够规范。这一节我讲讲我在拆包、组织依赖、脚本编排时的核心做法。2.1 包管理器与工作区选型直接影响部署体验先说结论我在 Vite Monorepo 组合里强烈推荐pnpm。npm workspaces 虽然也能用但在 Vercel 这种每次全新安装依赖的环境里pnpm 的硬链接机制和严格依赖隔离会让安装更快、构建更可复现。pnpm-workspace.yaml是 pnpm 工作区的声明文件示例packages: - apps/* - packages/*这里面有个细节如果 workspace 里既包含前端应用又包含共享库packages/*这个 glob 可能会把一些不应该被 workspace 管理的目录也包进来。比如你有个packages/scripts目录里面装的是一些 Node 脚本它不该被 app 直接引用。写了 glob 之后pnpm 会默认把目录下所有包都纳入 workspace容易在pnpm install时触发软链接解析问题。更严格一点我会显式排除集成测试目录、文档站点这类不应该进入生产构建依赖链的包packages: - apps/* - packages/* - !packages/e2e-tests - !docs/**这种排除规则在部署时有一个很实际的好处pnpm install时不会为无关包生成锁文件变更、不会多安装一堆不需要的依赖构建缓存命中率也能提高。2.2 package.json 里的 scripts 编排Monorepo 项目里的package.json不只是给自己看的Vercel 的构建命令会直接读取它。所以脚本设计必须兼顾本地开发、CI 和 Vercel 三套环境的语义。根目录package.json的 scripts 我是这样设计的{ scripts: { build:web: pnpm --filter web build, build:admin: pnpm --filter admin build, build: pnpm -r build, typecheck: pnpm -r typecheck } }在 Vercel 上给每个应用分别建 Project各自的 Root Directory 指向apps/web、apps/adminBuild Command 统一设为pnpm build。应用级别的package.json里的 build 脚本就是{ scripts: { build: vite build } }这样做的好处是Vercel 感知到的每个项目的命令都是简单的pnpm build实际执行什么完全由子包内部决定升级 Vite、换构建插件都不用在 Vercel 配置面板里来回改。2.3 Node 与包管理器版本锁定除了.nvmrc这个 Vercel 会自动读取更重要是让 Vercel 使用正确的 pnpm 版本。pnpm 9 之后对 lockfile 版本管理更严格偶尔会遇到这种情况本地是pnpm9.1.0生成 lockfileVercel 默认用的 pnpm 是 8.x构建时直接报 lockfileVersion mismatch。我在 Vercel 项目设置里用package.json的packageManager字段锁定版本{ packageManager: pnpm9.1.0 }Vercel 拿到这个字段后会自动安装对应版本的 pnpm不会出现本地和云端版本不一致的问题。这算是我踩完lockfileVersion坑之后总结出的最省心做法。3. Vite 构建配置在 Monorepo 中容易踩的暗坑Vite 本身非常适合 Monorepo它的构建是面向浏览器 ES Module 的天然支持跨包引用。但正因为 Vite 做了pre-bundling和按需加载Monorepo 场景下反而出现了一些单仓库不会遇到的陷阱。3.1 依赖预构建optimizeDeps与 workspace 包的纠葛Vite 在开发模式启动时会先用 esbuild 把依赖预构建成 optimized deps然后缓存在node_modules/.vite/deps里。这个机制在 Monorepo 里有个经典问题如果你直接在工作区里引用另一个包并且这个包package.json里main或exports指向的是 TS 源文件而不是构建产物Vite 会把它当作源码去解析但是默认不会对它进行预构建。常见的报错是[vite] optimized dependencies changed. reloading或者说某个 workspace 包里的依赖无法被正确解析。解决方式通常是在vite.config.ts里添加optimizeDeps.include// apps/web/vite.config.ts import { defineConfig } from vite export default defineConfig({ optimizeDeps: { include: [shared/ui, shared/utils] } })这个做法的原理是告诉 Vite这些 workspace 包里的模块你要提前为我处理掉。不加的话Vite 在首次访问页面时才会发现新依赖然后触发一整页 reload开发体验糟糕到怀疑人生。但在部署构建vite build场景下optimizeDeps.include不是构建错误的主要来源构建时 Vite 默认不做预构建用的是 Rollup 直接打包。真实的大坑是另外一个构建时 workspace 包的导出格式不兼容。3.2 workspace 包的导出格式与构建输出互不匹配我们有一个共享包company/ui它在package.json里定义成这样的 exports{ exports: { .: { import: ./src/index.ts, types: ./src/index.ts } } }本地开发一切正常因为 Vite Dev Server 会直接把 TS 源码转给浏览器执行。打包构建时问题就来了——Vite 在处理import { Button } from company/ui时会去读取company/ui的 package.json 的 exports发现指向的是.ts文件Rollup 确实能解析 TS但有时会出现 ESM/CJS 格式判断的混乱。我印象最深的报错是Rollup failed to resolve import company/ui/Button from src/App.tsx. This is most likely not possible in a monorepo setup.排查半天问题出在共享包里Button的导出路径Button不是通过入口文件的 re-export 导出的而是直接exports[./Button]指到子路径文件。Rollup 在 resolve 时对外部包的子路径解析遵循exports字段的精确匹配定义不全就解析不到。规范做法是共享包的package.json里这样写{ name: company/ui, main: ./dist/index.js, module: ./dist/index.js, types: ./dist/index.d.ts, exports: { .: { types: ./dist/index.d.ts, import: ./dist/index.js }, ./Button: { types: ./dist/Button.d.ts, import: ./dist/Button.js } } }也就是 Monorepo 里的共享库只要被 Vite 用于生产构建最好以构建后的产物为出口而不是直接把 TS 源文件暴露出去。虽然 pnpm workspace 开发时直接引用源码很爽但部署环境的解析逻辑和本地 Dev Server完全不同越早统一产物出口部署越省心。3.3 环境变量与import.meta.env在 Monorepo 中的传递Vite 的构建过程会读取.env文件默认读取的是项目根目录也就是vite运行的当前目录。Monorepo 里 Vercel 的 Root Directory 设成apps/web之后Vite 就会从apps/web下读取.env、.env.production。如果你的环境变量放在仓库根目录Vercel 上构建时会发现import.meta.env.VITE_API_BASE是undefined。这里要明确一个点Vercel 的 Environment VariablesProject Settings Environment Variables和本地.env是两套东西。Vercel 注入的环境变量会存在进程环境里Vite 默认不会把任意进程环境变量挂到import.meta.env上只有以VITE_前缀开头的变量才会被 Vite 暴露给客户端代码。所以正确做法是所有需要暴露给客户端的变量一律命名为VITE_XXX在 Vercel Project Settings 里配置这些VITE_XXX的值为生产环境对应的值本地开发的默认值放.env.development我遇到过同事把API_BASE_URL配在 Vercel 上前端代码里用import.meta.env.API_BASE_URL去拿结果生产环境永远为空build 又不会报错——这类问题排查真挺费神的。4. Vercel 上 Monorepo 的依赖安装与缓存机制每次部署都全量安装依赖是 Vercel 最容易被忽视的成本点。构建慢不说依赖安装阶段出问题还特别隐蔽。4.1 pnpm 的 store 机制与 Vercel 缓存Vercel 构建时会对依赖缓存做处理通常是根据 lockfile 的内容判断是否需要重新安装。对于 pnpm 项目Vercel 也会复用 cache 目录。但 pnpm 的安装有一个特点它会先在全局 store 里硬链接文件再链接到项目node_modules。Vercel 的缓存机制对 pnpm store 目录的保留策略有时候不太理想结果是重装依赖只快了一点点不太能达到秒级恢复的效果。如果构建时间太长解决思路是开启 Vercel 的Build Caching并且确保构建命令不输出随机内容有些工具会在构建产物里内嵌时间戳导致缓存每晚都失效。另外不要在构建命令里加--frozen-lockfile之外的额外安装参数时去强行改 lockfile。有一次我在 Vercel 上配 Install Command 为pnpm install --no-frozen-lockfile想着放宽依赖版本范围来解决问题结果是构建环境生成了新的 lockfile和仓库里的不一致导致后续所有部署都和本地依赖树对不上。为了一时省事换来的是长时间的不稳定挺不值的。Vercel 环境里应该用pnpm install --frozen-lockfile保证严格按 lockfile 安装。注意Install Command 一旦自定义Vercel 自动的依赖安装逻辑会被完全替换。很多缓存不生效的问题其实是自定义 Install Command 把 Vercel 内置的缓存逻辑给顶掉了。4.2 多 Project 共用一套 Monorepo 时注意构建隔离很多 Monorepo 会长出多个可部署应用管理后台、官网、用户端。在 Vercel 上逐个创建 Project每个的 Root Directory 指到不同子目录。这里有一个容易犯的错多个应用之间如果通过 workspace 共享代码它们的node_modules里有大量相同依赖。Vercel 的每个 Project 是独立缓存意味着同一套 pnpm store 会被复制多份。虽然不是致命的但每个 Project 首次构建都要重新解析所有依赖部署多个 Project 的总时间线性叠加这也算 Monorepo 上 Vercel 的一个隐性成本。相对有效的缓解方式让公共依赖尽量集中在少数几个 workspace 包中避免每个 app 各自引一堆重的依赖在共享包内部做 Tree Shaking 友好的导出设计减少 app 构建时需要打包的模块量4.3 使用output: server时要格外小心的 SSR 场景如果你的 Vite 应用不是纯静态 SPA而是 SSR比如用 Vite React Router 的 SSR 模式Vercel 的部署方式会大不一样。纯静态站可以用默认的 output 模式但 SSR 应用需要适配 Vercel 的 Function 机制。我试过的做法是通过vite-plugin-ssr或vite-plugin-react-router这类插件生成dist/server入口然后 Vercel 会把它包装成 Serverless Function。这里有个印象深刻的坑vite build的 SSR 构建产物是 CommonJS 格式时Vercel 的 Serverless 环境跑起来没问题但有时构建过程中 Rollup 会警告Module level directives cause errors when bundled. use client is not supported in CommonJS output.这个警告一般不会导致构建失败但如果你用了某个包同时发布module和main两种格式SSR 构建时可能选错格式最终在 Vercel Function 运行时报require is not defined之类的诡异错误。经验判断纯前端团队如果没有迫切的 SEO 需求Monorepo 里尽可能先把应用做成静态站output: static部署链路最简单Vercel 的全球边缘网络也能发挥最大优势。SSR 带来的复杂度是需要在部署层面额外买单的。5. 手工部署与 Git 集成用脚本补足 Vercel 配置的灵活性Vercel 的 Web 控制台很好用但 Monorepo 里常常需要自动化。Vercel CLI 在这时候就派上大用场了特别是配合 CI 流程。5.1 Vercel CLI 部署的多目录支持假设你要在本地手动部署apps/web命令很简单vercel deploy apps/web --prod它的原理是把apps/web目录作为项目根目录上传但这里有个坑Vercel CLI 上传的是单目录内容它不会自动带上仓库根目录的pnpm-workspace.yaml。也就是说如果你的apps/web依赖了 workspace 里的shared/ui但apps/web目录下没有pnpm-workspace.yaml它通常在根目录那么vercel deploy apps/web会创建一个只有apps/web内容的部署包pnpm install根本找不到 workspace 上下文构建就会失败。解决方式是用--cwd参数或者直接在仓库根目录执行cd /path/to/monorepo vercel deploy --cwd apps/web --prod但实测下来CLI 对 Monorepo 的支持比较有限术语上叫法也不统一。如果构建配置复杂我建议直接走 Git 集成push 触发部署而不是用 CLI 处理本地 Monorepo。5.2 Git 集成时的分支与忽略规则Vercel 的 Git 集成默认对 main 分支自动部署生产环境对其他分支创建 Preview Deployment。Monorepo 场景下一个仓库里有多个应用这样就会有个问题你只是在packages/shared里改了一行代码Vercel 也会触发所有 Project 的构建。浪费构建时长不说改动如果无意中影响了多个 app部署面也被不必要地放大了。Vercel 针对这个场景提供Ignored Build Step配置。我踩过几次坑后给每个 Project 都配了脚本只有相关目录有变更时才真正构建。比如在apps/web的 Project Settings 里设置 Ignored Build Step 为git diff --quiet HEAD^ HEAD -- apps/web packages/shared pnpm-lock.yaml一行命令当apps/web、packages/shared或 lockfile 没变化时Vercel 会跳过这次构建直接复用上一次的部署。这个技巧对 Monorepo 特别实用铺开之后你会发现 Preview 部署的数量锐减构建列表面貌清爽不少。5.3 从构建日志中定位 Monorepo 特有报错最后分享一个排查技巧如果 Vercel 构建日志里报了 Failed to resolve entry for package赶紧去查package.json的exports和main字段。这类报错在 Monorepo 里八成是 workspace 包的产物路径配置和 Vite 的解析规则对不上。如果报的是 out of memory 或者 JavaScript heap out of memory先看构建命令里有没有显式设置 NODE_OPTIONSNODE_OPTIONS--max-old-space-size4096 vite buildMonorepo 打包多个应用依赖时Rollup 的 AST 和模块图会占不少内存。Vercel 默认的内存限额是 4GB 左右大型项目很容易顶满。我遇到过某项目本地 8GB 内存构建正常到了 Vercel 就 OOM加上NODE_OPTIONS限制之后反而更稳定。所以有时候限制内存反而比无限制超卖更可控有点反直觉。6. 项目设置面板里的关键决策点Vercel 控制台的项目设置页面每个选项看起来都简单组合起来却是天壤之别。我梳理一下 Vite Monorepo 部署时最关键的几个设置项。6.1 Root Directory 的取舍这一节再强调一次因为太重要了。配 Root Directory 时Vercel 会重新计算你的 Project 的工作目录这个决定了package.json的读取位置lockfile 的查找位置.env的加载目录构建日志中相对路径的起始点对于apps/web项目Root Directory 就应该填apps/web。同时注意Vercel 的 Framework Preset 应该选 ViteBuild Command 保持pnpm build时会自动拼接为cd apps/web pnpm build无需自己拼。6.2 Build Command 的干净化Build Command 不是越复杂越好。我见过有人把构建命令写成长长的 Bash 拼接动不动就cd ../.. pnpm install pnpm --filter web build这种写法反而容易出问题。一个合格的 Build Command 应该是「简短、明确、不跨目录」pnpm build其他的前置操作放在应用的package.json的prebuild脚本里{ scripts: { prebuild: pnpm --filter shared/ui build, build: vite build } }这样每当执行pnpm build时pnpm 会先执行 prebuild把依赖的共享包构建好再开始打包应用。这个语义在本地、CI、Vercel 是完全一致的不会因为是 Vercel 环境才生效。这是我认为 Monorepo 部署时性价比最高的收敛点。6.3 Vercel Labs 新特性的留意价值Vercel 团队一直在迭代部署基础设施特别是 Monorepo 场景下的缓存与依赖分析。我在浏览 Vercel Labs 相关功能时看到他们最近比较关注Scripts API和缓存细粒度控制方向的东西。具体来说Scripts 可以将部分构建行为脚本化让部署前的检查、产物后处理更规范地纳入版本管理而不是散落在 Vercel 面板的多个设置项里。这对 Monorepo 的意义在于不同 app 之间可以复用同一套部署检查逻辑分支保护、产物校验、缓存策略都能用代码表达。这不是广告我的真实看法是项目早期的部署配置完全可以留在面板里但一旦 app 数量接近 3 个把配置往代码仓库转移是迟早的事。等你发现要在三个 Project 面板里手动同步同一份配置时自然就懂了。7. 完整实操一个双应用 Monorepo 从零部署到 Vercel前面讲了这么多原理和规则这里放一个可以直接照着做的完整例子。我以一个包含web和admin两个 Vite 应用的 Monorepo 为例说明整个流程。7.1 仓库结构初始化假设仓库结构是这样的my-monorepo/ ├── apps/ │ ├── admin/ │ │ ├── package.json │ │ └── vite.config.ts │ └── web/ │ ├── package.json │ └── vite.config.ts ├── packages/ │ └── ui/ │ ├── package.json │ └── src/ ├── package.json ├── pnpm-workspace.yaml └── turbo.json根目录的package.json{ name: my-monorepo, private: true, scripts: { dev: turbo run dev, build: turbo run build }, devDependencies: { turbo: ^2.0.0 }, packageManager: pnpm9.1.0 }packages/ui/package.json的关键配置产物导出规范{ name: my/ui, version: 1.0.0, main: ./dist/index.js, module: ./dist/index.js, types: ./dist/index.d.ts, exports: { .: { types: ./dist/index.d.ts, import: ./dist/index.js } }, scripts: { build: vite build --config ../../vite.lib.config.ts } }注意这里共享库也走 Vite 构建配置放在仓库根目录的vite.lib.config.ts设置build.lib和build.rollupOptions.output生成 ESM 产物。这种模式下vite build的入口就是packages/ui/src/index.ts输出到同目录下dist。7.2 Vercel 上分别为两个应用建项目在 Vercel 控制台创建 Project导入同一个 Git 仓库然后注意以下设置Project Name:my-monorepo-webRoot Directory:apps/webFramework Preset: ViteBuild Command:pnpm buildOutput Directory:distVite 默认输出到distVercel 会自动识别但确认一下没坏处Install Command:pnpm install --frozen-lockfileapps/web/package.json的关键脚本{ scripts: { prebuild: pnpm --filter my/ui build, build: vite build } }admin应用如法炮制Root Directory 填apps/admin即可。7.3 关键构建链路验证部署时Vercel 实际执行的逻辑是进入apps/web目录读取这里的package.json发现packageManager: pnpm9.1.0自动安装对应 pnpm执行pnpm install --frozen-lockfile通过 workspace 上下文安装根目录和子包的全部依赖执行pnpm build先触发 prebuild构建my/ui再执行vite buildVite 在打包时通过my/ui的exports字段找到dist/index.js完成打包产物输出到distVercel 收集并发布这套链路的关键地方是第 3 步因为apps/web目录下有packageManager字段但同时需要 workspace 上下文所以pnpm 必须在仓库根目录或能向上找到 workspace 的地方执行。Vercel 对 root directory 的支持方式确保了pnpm install能发现位于仓库根目录的pnpm-workspace.yaml。实测中如果你的 root directory 是apps/web而node_modules在根目录那么pnpm install可能不识别。这个情况下可以把 install command 改成cd ../../ pnpm install --frozen-lockfile但这样就违背了我前面说的保持命令简单原则。更优雅的解法是把pnpm install默认就放在根目录执行Vercel 的 workspace 感知机制通常能处理好。如果确实不行我推荐用如下 Install Commandnode -e const { execSync } require(child_process); execSync(pnpm install --frozen-lockfile, { cwd: ../../, stdio: inherit })这一串只是在 Vercel 的局限下绕了个弯不推荐作为首选但是应急时能救命。7.4 部署完成后的验证清单部署成功后建议开一个浏览器无痕窗口逐项确认页面是否 200静态资源是否从/assets/xxx.js正常加载控制台有没有Failed to fetch dynamically imported module路由刷新比如直接访问/about不 404最后一条很关键。Vite 的 SPA 默认使用 History 路由Vercel 需要把/about这类路径重写到index.html。Vercel 对 Vite 项目会自动处理 rewrites但如果你在某些 devDependency 或配置里动了它刷新 404 就会突然出现。可以在项目根目录放一个vercel.json{ rewrites: [{ source: /(.*), destination: /index.html }] }不过如果 Root Directory 是apps/webvercel.json应该放在apps/web下不然不生效。8. 实战中遇到的高频报错与解决办法这里整理的报错是我在 Vite Monorepo Vercel 组合下真实遇到过的按出现频率排序每个都附了定位思路。8.1ERR_PNPM_LOCKFILE_MISMATCH或 lockfile 版本错误现象构建日志提示当前 pnpm 版本无法解析 lockfileVersion或者lockfile needs to be updated。原因本地的 pnpm 版本和 Vercel 的 pnpm 版本不一致生成的 lockfile 格式存在差异。对策在根目录package.json里写死packageManager字段例如packageManager: pnpm9.1.0同时把pnpm-lock.yaml提交到 Git。下次 Vercel 构建时它会自动使用指定版本 pnpmlockfile 就不会再闹脾气。8.2vite: not found现象构建报sh: vite: command not found或类似 vite 无法识别。原因在 Monorepo 里执行构建命令的工作目录不是 Vite 应用所在的目录或者安装依赖时没有安装应用层级的依赖。对策先确认 Root Directory 是否指向应用目录再检查 install 是否真的执行成功然后确认应用本身的package.json的 devDependencies 里有vite不要把 vite 放在根目录 devDependencies 里就万事大吉pnpm --filter执行时依赖解析还是要看应用自身的依赖声明。8.3Module not found: Error: Cant resolve shared/ui现象构建时 Vite 解析不到 workspace 包。原因workspace 包的exports字段没有给出合理的产物路径或者该共享包没有在之前构建好。对策按 3.2 节的内容检查shared/ui的package.json的exports确保 build 命令里通过 prebuild 或 turbo 先构建共享包。8.4Deployment failed: no output directory named dist detected现象构建成功了但 Vercel 没找到可以发布的产物目录。原因Vite 输出目录被改过比如改成了outDir: build或者 Output Directory 和实际产物位置不一致。对策确认 Vite 配置里的build.outDir是什么然后同步修改 Vercel 的 Output Directory 设置。如果不想每次改配置建议 Vite 输出统一固定为dist。8.5JavaScript heap out of memory现象构建中断日志末尾有 heap OOM 信息。原因Monorepo 多包同时被 Vite 打包或者某个共享包被重复打包成多份导致 Rollup 内存暴涨。对策Vercel 环境变量NODE_OPTIONS设为--max-old-space-size4096或者在 build 命令前显式设置。同时检查是否引入了体积异常大的依赖比如完整的lodash而不是lodash-es用rollup-plugin-visualizer分析产物构成砍掉不必要的依赖。9. 我最后想分享的几条心得回到开头那句话——本地跑得好好的项目部署到 Vercel 挂了基本上都是环境假设不一致造成的。本地的 node_modules 里有半年前装的依赖缓存Vercel 是全新的本地用了 Node 20Vercel 默认 Node 18本地直接pnpm --filter web buildVercel 可能用了另外的脚本。理解这一点排错就成功了一半。我在实际项目里形成的两个硬性习惯第一所有部署相关命令安装依赖、构建、类型检查都放在package.json里并且保证它们在本地可复现。也就是说任何时候手动跑命令都要能获得和 Vercel 一样的结果这样在本地就能提前发现部署问题。第二每次调整部署配置一定要保留一次完整成功的构建日志作为基线。之后任何改动如果导致失败先和基线对比差异不要急着改配置。很多 Vercel 的面板配置项是组合生效的改动一个可能影响其他行为有基线比对会安全很多。Vite 的构建哲学和其他打包器有很大不同。它是一个极简入口加超强插件的组合开发者的自由度极大但这也就意味着构建链路受工程结构影响远大于传统 Webpack 模板。Monorepo 解决的是代码组织问题Vercel 解决的是部署环境问题两者碰在一起中间的所有缝隙都需要你理解原理后主动去填。这些缝隙填好了整个部署链路其实可以很流畅——一次配置全仓库多个应用并行发版体验确实很香。希望这篇记录能帮你在部署路上少走些弯路。如果你也踩过什么特别刁钻的坑欢迎在评论区分享大家互相补全一套 Monorepo 部署避坑手册比各自在日志里挣扎要有用得多。