
我前阵子帮一个团队评估新项目的构建方案。需求并不复杂TypeScript 写的后台管理系统有组件库、工具函数还要把几个模块抽出来单独发布。问到 Webpack 几乎是本能的毕竟团队所有存量项目都基于它文档熟练踩坑经验也足够。但聊得越深我越清楚一件事真正让人想换掉 Webpack 的往往不是它“不能用”而是它的配置成本、启动成本和心智负担。这个议题不算新鲜但今年情况确实不太一样——TypeScript 已经接近默认基础设施Vite 覆盖了大量应用开发场景tsup 把库构建简化到近乎普通脚本Rolldown 又把“Vite 底层换核”从话题变成了可尝试的路径。所以我更愿意把这次思考描述成一个判断这一轮工具更替重点不是“打包更快”而是把之前被 Webpack 硬塞进一个配置文件里的多件事拆开了。Webpack 像一个什么都管的大管家能力边界宽但任何一件事想改顺手都要先理解那套相当庞杂的规则。而 tsup、Vite、Rolldown 的思路更接近分工协作开发时用 Vite 的即时反馈发布库时用 tsup 的干净产物底层打包交给 Rolldown 或 esbuild 这类更高效的引擎。“告别 Webpack”不一定意味着否定它而是意味着你的构建流程不再由一个庞大的配置系统统一接管。1. 先别急着告别Webpack 教会了我们什么又在哪里透支了体验1.1 Webpack 当年解决的核心问题回到 Webpack 盛行的时代前端工程化面临的问题和今天完全不同。那时模块规范还在过渡浏览器对 ES Module 的支持能力有限开发者需要一套机制把散落各处的 JS、CSS、图片、字体统一处理。Webpack 的价值不只是“打包”而是提供了一个统一资源处理模型loader 负责把各种资源转成可打包的模块plugin 负责在打包生命周期里做优化、注入、拆分。这个模型最厉害的地方在于它把不确定的外部资源世界收敛成一个可编程的构建流程。直到今天很多团队仍然依赖 Webpack 的 loader 体系和插件生态来解决特殊需求。比如样式预处理、图片压缩、代码混淆、产物注释清理、按需加载、多页面打包、复杂的环境变量注入。这些能力是真实的也是很多项目正常运行的基础。任何一种替代方案在宣传自己“更快”之前都得先回答一个问题你的生态能不能覆盖这些实际场景从历史作用来说Webpack 完成了前端工程化的一次大统一。正因为它太成功后来才有大量项目建立在对它的路径配置、loader 顺序、plugin 实现细节的依赖之上。理解了这一层你才能理解“告别 Webpack”为什么不能只是一次简单的工具替换。1.2 想更换工具的三种真实信号不是所有项目都需要换工具。我见过不少团队把 Webpack 配置优化得很好构建速度也在可接受范围那确实没必要为了追新而追新。但如果你遇到下面三种信号就该认真考虑新的组合方案了。第一种信号配置增量已经超过业务增量。一个新页面不需要写几行业务代码反而要先搞清楚“为什么这个 svg 走不了”、“为什么新增的目录没被打进产物”、“为什么 node_modules 里的某个包又被编译了一遍”。这种时候工具已经从基础设施变成了日常阻力。第二种信号dev server 的启动和热更新时间开始影响开发节奏。Webpack 的缓存策略和持久化缓存配置可以解决一部分问题但它的架构决定了全量编译和复杂依赖图的处理开销很难被彻底消掉。当改动一个文件要等 5 到 10 秒甚至更久开发状态就会被打断。很多人对 Webpack 的抱怨不是“打包慢”而是“等待感太强”。第三种信号你希望同一份代码既能作为应用的一部分运行又能被独立发布成库或组件给其他项目使用。Webpack 虽然也支持 library 模式但配置起来并不轻松还要处理 externals、UMD、ESM 产物兼容等问题。相比之下用 tsup 这类面向库构建的工具几乎可以在一份不到十行的配置里完成同样的事情。1.3 一个容易被忽略的干扰项有些“ts”和 TypeScript 无关搜索“TS”这个关键词时经常看到两类完全不同的内容一类是 TypeScript另一类是视频传输流中的分片文件。比如“下载视频变成了很多 ts 文件”、“ffmpeg ts 伪装 jpg”、“带 subtitle 的 ts 流”这些和前端工程化没有任何关系。它们只是恰好用了同一个缩写。我提这件事是因为工具选型时最怕被错误信息干扰。如果你发现自己绕进“TS 不是 TypeScript”的检索迷宫里先退出来把自己要找的东西定义清楚。这篇文章里的 TS全部指 TypeScript。想替换 Webpack 不是情绪问题而是判断问题。你要先看清当前项目到底被什么困住再决定新方案承担什么角色。2. 组合拳不是堆工具tsup、Vite、Rolldown 如何分工2.1 三层分工模型应用构建、库构建、打包内核很多人一开始容易做错一件事把 tsup、Vite、Rolldown 当成“三个 Webpack 替代品”来回对比然后试图选一个赢家。实际上这三者在当前生态里不是同一个层级的东西它们解决的问题也不一样。一个相对清晰的分层方式是应用层你用 Vite 启动开发服务器、代理接口、编译应用代码、产出 web 应用所需的静态资源。库层你用 tsup 打包可复用代码比如组件库、工具函数、请求封装、类型定义输出 ESM 和 CJS 双格式同时生成声明文件。底层内核Rolldown 或 esbuild 负责真正执行打包、转译和代码转换的算法。Vite 是一个完整的应用开发与构建工具它在开发阶段用 esbuild 做依赖预构建在生产构建阶段用了 Rollup。tsup 则是一个更聚焦的“库打包器”它的底层早期依赖 esbuild后来也能接入别的底层能力。Rolldown 更偏向底层它想用 Rust 重新实现一个和 Rollup API 兼容的打包内核未来可能替代 Vite 底层依赖的 Rollup。用生活里的例子类比Vite 像一套精装房的施工流程从进场到交付都管tsup 像一个家具工厂负责把某些标准模块做成可搬走的成品Rolldown 更像生产流水线上的核心机床效率更高但它本身不直接面向最终用户。这套分工模型想传递的核心观点是工具组合的价值不在工具数量多而在于每个工具只承担自己最擅长的一层。你不需要把库构建和应用构建的所有配置都堆在同一个工具里。2.2 为什么会出现 Rolldown 这种“底层换核”方案要理解 Rolldown得先理解 Vite 现在的一个隐性摩擦。Vite 在开发阶段使用 esbuild 做依赖预构建和 TS 转译速度非常快但在生产构建阶段使用 Rollup因为 esbuild 的产物控制和代码分割能力还达不到完整替代 Rollup 的程度。这带来一个客观问题开发环境和生产环境的模块处理逻辑并不完全一致。很多“开发环境正常打包后报错”的诡异问题根源就在双引擎差异。Rolldown 的思路是用 Rust 重写一个 Rollup 兼容的打包内核它既保留 Rollup 的插件生态和产物模型又能获得接近 esbuild 的性能。如果这个方向成熟Vite 的开发和生产构建可以共用同一套内核从而消除偏差。这也就是“只要换一个底层整个上层体验都会变化”的逻辑。需要强调Rolldown 不是基于 esbuild 的简单替代也不是 Vite 的替代方案。它是给 Vite 这类构建工具提供更优底层的实验性项目。从公开进展看它已经有部分里程碑版本发布但距离“所有生产项目无感切换”仍有距离。落地前务必以官方文档和最新 changelog 为准。2.3 这套组合适合谁不适合谁先说适合的场景。如果你是新手项目、中小型 Web 应用或者团队正在做技术栈统一、需要同时维护应用和多个库那么 tsup Vite 的组合非常值得尝试。它配置量小、反馈快、默认行为清晰很容易把一个项目的构建流程控制在可理解的范围内。对于组件库、工具包、接口层封装这类产物tsup 几乎是当前最省心的选择之一。再说不适合的场景。如果你有一个大型存量 Webpack 工程里面用了大量自定义 loader、复杂 plugin、动态 require、依赖内部模块路径等写法那么“告别 Webpack”的成本会非常高。这种情况下更合理的策略不是推翻重来而是渐进拆分先把可以被标准工具处理的模块抽离出去再逐步把剩余部分迁移到新工具上。如果你只是被之前某个项目里的 Webpack 配置搞烦了但新项目并不需要多库发布、组件复用和快速迭代那组合拳的优势就不明显。工具是为你服务的不是用来证明你技术很新的装饰品。3. 落地基线TS Vite tsup 的最小工程怎么搭3.1 项目结构和依赖准备我先给一个常见的最小结构。这里以“应用 独立库”并存的单仓库简化版为例展开避免一上来引入复杂 monorepo 概念。your-project/ ├─ src/ │ ├─ components/ │ ├─ pages/ │ ├─ utils/ │ ├─ api/ │ └─ index.ts ├─ package.json ├─ tsconfig.json ├─ tsup.config.ts └─ vite.config.ts依赖准备上核心的大类如下TypeScript提供类型系统和 tsc 命令。Vite 以及对应框架插件比如 Vue 项目用vitejs/plugin-vueReact 项目用vitejs/plugin-react。tsup负责库构建。项目自己的业务依赖和开发依赖建议放在 package.json 中明确区分。安装方式很常规直接使用 npm、pnpm 或 yarn 按项目规范安装即可。需要注意 Node 版本和包管理器版本不同工具对运行环境的支持范围不同。如果原始材料没有给出明确版本落地前要先确认依赖版本不要拿旧版本硬套新配置。3.2 tsconfig先做类型检查再做打包类型检查是 TypeScript 项目的地基。很多人在 Vite 项目中把类型检查完全交给 Vite这是不对的。Vite 用 esbuild 转译 TS 时只做语法转译不会做完整的类型检查。也就是说只要语法合法即使类型有问题也能跑起来。所以 tsconfig 至少要包含以下关键点{ compilerOptions: { target: ES2020, module: ESNext, moduleResolution: Bundler, strict: true, skipLibCheck: true, esModuleInterop: true, forceConsistentCasingInFileNames: true, resolveJsonModule: true, isolatedModules: true, declaration: true, baseUrl: ., paths: { /*: [src/*] }, outDir: dist }, include: [src] }几个容易踩坑的点moduleResolution设为Bundler是因为 Vite、tsup 这类打包器使用 ESM 解析逻辑它比Node更贴近实际运行环境。isolatedModules启用后要求每个文件都能被单独转译这比较符合 esbuild 的单文件转译模式。strict建议一直开着它会在编译阶段拦截大量低级错误。至于热词里经常出现的TS interface、TS partial、TS 封装 axios 设置响应返回类型其实都指向同一个观点类型定义是团队的约定文档而不是装饰。一个项目想要长期维护首先要让npm run typecheck成为提交代码之前必跑的检查项。3.3 tsup 配置库构建优先tsup 的魅力在于默认配置足够聪明你需要关心的字段不多。下面是一个常见的库构建配置import { defineConfig } from tsup export default defineConfig({ entry: [src/index.ts], format: [esm, cjs], dts: true, sourcemap: true, clean: true, splitting: false, treeshake: true, external: [vue, react], })逐项解释一下entry库入口文件通常是你希望对外暴露的公共 API。format输出 ESM 和 CJS 两种格式。这样既支持现代构建工具也能兼容老式 Node 项目。dts生成.d.ts类型声明文件。这一步对库发布来说很重要没有类型声明使用方在 TS 项目里会非常痛苦。sourcemap生成 sourcemap方便使用方调试库代码。clean每次构建前清空输出目录避免旧文件残留。splitting库构建时一般关闭代码分割避免产物碎片化。external把 vue、react 这类框架依赖标记为外部依赖不打包进产物由使用方项目自己提供。注意external不是在配置里随便列两个包名就完事。你要先想清楚哪些依赖应该打进产物哪些应该交给使用方。打包进产物会让库体积变大也容易造成多实例问题不打包则会要求使用方必须安装对应依赖。在具体项目里如果你还负责工具函数或接口封装可以给每个包单独维护一个 tsup 配置甚至用多入口一次构建多个产物。但不要一上来就把所有模块塞进同一个入口先保持入口稳定再逐步扩展。3.4 Vite 配置应用构建的核心Vite 配置更偏应用侧。以一个 Vue 项目为例常见配置可以长这样import { defineConfig } from vite import vue from vitejs/plugin-vue import { fileURLToPath, URL } from node:url export default defineConfig({ plugins: [vue()], resolve: { alias: { : fileURLToPath(new URL(./src, import.meta.url)) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } }, build: { outDir: dist, sourcemap: true } })这里要重点理解两个部分。resolve.alias决定了代码里的/components/xxx指向哪里。它必须和 tsconfig 里的paths保持一致。如果 tsconfig 里配了/*但 Vite 里没配开发时也许能通过但类型检查和构建时可能就找不到模块。保持两边一致是减少配置冲突的关键。server.proxy解决了开发阶段的接口跨域和代理问题。它把前端请求转发到后端服务从而让浏览器认为所有请求都来自同一个源。具体到某个/api/form/list请求失败时往往不是 Vite 配置本身的问题而是 target 指向的服务不可用、路径没有正确 rewrite、或者后端 CORS 策略太严格。3.5 从单命令到开发、构建、类型检查、产出的闭环配置好之后你需要的不是一个命令而是一组命令形成闭环# 类型检查 npm run typecheck # 开发环境 npm run dev # 应用构建 npm run build # 库构建 npm run build:lib如果项目把类型检查放在npm run build之前那 Vite 的打包就不会被类型错误悄悄带过。一个更稳的流程是先跑 typecheck捕获类型问题。再跑单元测试或 lint保证逻辑和规范。然后分别跑 build 和 build:lib验证应用产物和库产物都正常。最后检查 dist 目录里的文件名、sourcemap、声明文件是否齐全。这一步看起来简单但它决定了你的构建体系是“能跑”还是“可维护”。4. Rolldown 的正确打开方式实验性内核怎么理解、怎么试4.1 为什么需要 Rust 重写打包内核Rolldown 出现之前前端社区对构建性能的追求已经分成了两条路线一条是 esbuild 的“尽可能快”另一条是 Rollup 的“尽可能符合生态”。esbuild 很快但插件模型和产物控制比 Rollup 弱Rollup 生态成熟但 JavaScript 实现的性能在大型项目上有瓶颈。Rolldown 想同时拿到两者的优点用 Rust 提供高性能同时保持 Rollup 兼容的 API让大量现有插件不需要重写就能继续使用。这个方向一旦成立Vite 的双引擎问题就会从根上解决。更重要的是它把“构建工具”这件事从配置竞争提高到了“内核性能”竞争的层面。4.2 在 Vite 中切换 Rolldown 的试验路径因为 Rolldown 仍处于快速迭代阶段不同版本入口和启用方式可能都不一样所以我不在这里写死命令。更稳妥的做法是把它当作一个实验分支来评估。一个通用试验路径复制一份当前项目或者新建一个空项目不要直接改生产分支。查看 Vite 和 Rolldown 官方文档、发布说明确认当前版本推荐的启用方式。在最小范围内验证插件兼容性。先跑通最简单的页面再逐步加组件库、路由、状态管理、接口代理。对比开发启动时间、热更新时间、生产构建时间和产物大小记录数据。在真实评估过程中你会很快发现哪些插件和 Rolldown 有兼容问题哪些构建行为发生了变化。这类信息比看任何评测文章都有价值。注意不要因为 Rolldown 性能数据亮眼就直接把它引入重要生产环境。它更像是一辆性能很强的工程样车适合在测试赛道里充分验证不适合第一天就开着它上高速。4.3 用 Rolldown 之前的边界检查如果你想在项目里引入 Rolldown建议先回答这几个问题你的项目版本和依赖版本是否支持当前 Rolldown 的启用方式你依赖的 Vite 插件是否都兼容 Rolldown 的插件模型项目的开发构建和生产构建是否都跑过一遍完整验证有没有做性能对比和产物差异对比万一出现严重问题回退到 Rollup 或 esbuild 的路径是否顺畅如果有一个问题回答不了就不要把它推向生产。用工具不是为了追踪最新版本而是为了获得稳定可控的交付流程。4.4 现在不建议做的几件事以目前的成熟度我不建议做下面几件事不建议把 Rolldown 直接作为默认工具写进新项目模板除非你的团队愿意持续处理版本变化。不建议为了兼容 Rolldown 去改造现有插件链这等于把迁移成本提前支付给了还不稳定的底层。不建议只因为 Rolldown 性能高就忽略构建结果差异性能不是衡量构建工具的唯一指标。这一章并不是否定 Rolldown而是强调边界。工具越靠近底层越需要谨慎评估。5. 从 Webpack 迁移的实操路线先抽层再接管最后换核5.1 第一步把可复用代码用 tsup 抽成独立包从 Webpack 项目迁到新组合最大的风险是“一刀切”。正确做法是先抽层。先找出项目里可以被独立复用的部分工具函数、通用组件、类型定义、请求封装。把它们放到独立目录或独立包中用 tsup 打包。这样带来的第一个好处是这些代码不再被应用构建捆绑可以单独测试、单独发布。第二个好处是后续应用迁移 Vite 时不需要处理这部分复杂依赖。这个阶段最容易遇到的问题是多包之间的依赖关系。如果 A 包依赖 B 包并且通过相对路径直接引用 B 包源码那再好的工具方案都会变得很脆弱。建议先明确包的对外入口让包之间只通过入口文件依赖。5.2 第二步用 Vite 接管应用开发服务器和构建抽离出可复用代码后下一步是让应用开发从 Webpack 切到 Vite。这一步不用追求一步到位。可以先把开发服务器切到 Vite保留旧的 Webpack 构建作为发布构建用一段时间对比差异。开发阶段主要关注热更新是否正常、接口代理是否一致、环境变量是否完整、静态资源是否符合预期。如果开发体验稳定再逐步把生产构建也切换过去。这里要特别注意环境变量的替换规则。Webpack 里可能用process.env.NODE_ENV或自定义插件注入变量Vite 则默认通过import.meta.env暴露变量并用loadEnv读取.env文件。迁移时需要把老的环境变量格式梳理一遍避免构建后出现 undefined。5.3 第三步评估 Rolldown 替换时机当前两步都稳定之后再考虑 Rolldown。评估的关键时刻是你的 Vite 插件生态已经稳定构建产物确认没有异常团队对 Vite 的配置和维护已经有足够经验。此时引入 Rolldown主要目的是解决你确实遇到的构建瓶颈而不是为了追新。没有明确瓶颈就保持现状。Rolldown 的替换时机不是由技术热度决定的而是由稳定性和收益共同决定的。如果现有方案已经能让你把时间和精力放在业务上那它就是一个好方案。5.4 一份迁移检查清单迁移到新构建体系之前建议把这张检查清单过一遍。下面这些项是我在多个项目里都会确认的内容检查项需要确认的问题依赖版本Node 版本、包管理器版本、Vite/tsup/Rolldown 版本是否兼容入口与出口应用入口、库入口、输出目录和文件名是否清晰环境变量所有process.env和.env变量是否迁移到import.meta.env或统一配置静态资源public 目录内容是否完整资源路径是否发生变化代理规则开发代理是否与后端接口地址匹配是否启用了 rewrite插件替代Webpack loader 和 plugin 是否找到 Vite/Rolldown 对应方案产物清理是否清空旧 dist避免旧文件与新文件混在一起类型声明库产物是否生成 d.ts应用类型检查是否独立执行构建缓存CI 和本地构建缓存是否清掉避免旧缓存干扰回归验证关键页面、接口、鉴权、路由是否全部走一遍还有一个容易忽略的细节Webpack 时代很多团队会配置注释清除或代码压缩相关的优化项比如terser的format.comments、optimization.minimizer。切到 Vite 后注释清理和压缩策略是由 Vite 自身或其生成的底层配置决定的方法和 Webpack 完全不同。迁移时要主动检查产物里是否保留了不该保留的注释、签名或调试信息不能默认“新工具一定会做”。注意迁移过程中尽量把“工具替换”和“业务重构”分开。不要一边换构建工具一边改业务模块结构。两件事同时发生时你很难判断问题来自工具还是来自业务改造。6. 高频故障排查链路三个真实报错一层一层查6.1 问题一Cannot find package vite我看到这个报错出现在很多新手项目里。现象是运行npm run dev时Node 提示找不到 vite 包有时还带着imported from的路径。先别急着重装依赖。排查顺序应该是打开 package.json确认 vite 是否真的出现在 devDependencies 或 dependencies 里。检查当前目录位置确认你是在项目根目录运行的命令而不是子目录。检查 node_modules 里是否存在 vite 目录。如果存在检查版本是否与 package.json 声明一致。如果使用 pnpm还要确认是否因为工作区 hoisting 导致依赖被安装到了根目录而子项目访问不到。确认没有 lockfile 错乱后再决定是否删除 node_modules 和 lockfile 重新安装。最忌讳的是直接运行npm install vite它可能把 vite 装到不正确的层级或者安装了错误版本反而把问题弄复杂。依赖问题要先看“依赖应该在哪里”再看“实际在哪里”最后才动手调整。6.2 问题二[vite] http proxy error这个报错在开发环境里很常见。比如/api/form/list?page1pagesize10请求代理失败。看到http proxy error时首先要意识到它通常不是 Vite 配置写错而是代理链路里某个环节没通。按下面几步排查先确认代理目标服务是否在运行。可以直接用 curl 请求http://localhost:3000/api/form/list看后端有没有响应。检查 vite.config.ts 里的proxy.target是否正确端口是否写错。检查是否需要 rewrite 路径。如果后端接口不带/api前缀就需要把前缀去掉。检查changeOrigin是否开启尤其是后端服务对 Host 头有校验时。检查本地是否使用了localhost而代理目标是 IPv6 地址或特定域名解析可能存在差异。可以开启构建工具的调试日志观察代理请求实际发到了哪里。这类问题影响面很大但定位路径其实很固定先确认目标服务再确认代理规则最后才怀疑工具本身。6.3 问题三public 目录与旧构建配置冲突有一个常见提示是content not from webpack is served from public这是 Webpack dev server 对 public 目录内容的说明。如果你在新项目里仍然看到类似信息说明你可能还跑在旧构建体系下或者旧构建脚本没有真正被替换。另一个常被忽略的问题是一个 dist 目录同时被 Webpack 和 Vite 写入。旧脚本还在跑构建新脚本也在写产物最后目录里的文件新旧混杂很容易出现“新代码没生效”的错觉。遇到这种情况先关闭所有构建进程清空 dist 目录再确认当前 dev 和 build 命令实际执行的是哪个配置文件。检查 package.json 里的 scripts检查有没有残余的 webpack.config.js 或老启动脚本。public 目录本身的处理逻辑在 Vite 里比较清晰所有放在 public 下的文件都会原样复制到构建输出根目录并且放在 public 之外的静态资源通过 import 或 URL 引用。迁移时要注意路径变化尤其是 favicon、page 模板这类非 JS 资源。6.4 通用排查顺序表把上面的经验收拢成一张通用排查顺序表以后遇到“构建工具行为异常”时可以按这个顺序走阶段要做的检查典型问题现象到底报什么错、卡在哪里、输出什么报错信息被忽略直接猜配置输入入口文件、路径、编码、格式是否正常文件路径大小写、文件损坏、编码不一致环境Node、包管理器、依赖位置、版本依赖安装层级错误、锁文件错乱配置入口、出口、别名、代理、环境变量别名不一致、代理 target 错、环境变量缺失资源public、静态文件、构建缓存旧资源残留、缓存干扰工具边界插件兼容、已知缺陷、版本限制插件不支持新版本、实验性功能不稳定多数“诡异”问题都能在输入或环境环节找到原因。别一上来就怀疑工具能力不够。回到最初那个团队的问题我的建议其实很简单先在一个独立分支或新项目里把 tsup Vite 的组合跑通再思考是不是真要引入 Rolldown。如果你发现开发链路变短了、构建行为变清晰了、产物结构变稳定了那这次工具更新就算成功。真正值得长期关注的不是“用上了某个新打包器”而是你是否有能力控制构建流程的每一步。工具总在迭代但“先跑通、再分流、再优化”的处理方法才是这一类方案留给你最持久的东西。