💡 引言
在传统构建工具(如 Webpack)统治的时代,项目一旦变大,本地开发启动和热更新往往需要数秒甚至更久。Vite 凭借"天生不用打包"的原生 ESM(ES Modules)机制横空出世,带来了毫秒级的极致体验。
然而,很多同学在刚接触 Vite 时都会有一个疑问:既然 Vite 宣称是不打包的构建工具,为什么在开发阶段启动时,终端里总会雷打不动地出现一行 Pre-bundling dependencies(依赖预构建)?
事实上,面对庞大复杂的第三方生态(node_modules),Vite 宁可破坏"不打包"的纯洁性,也要引入 esbuild 进行一次特殊的预加工。这既是 Vite 的妥协,也是它极其精妙的工程化智慧。本文将带你一层层剥开预构建的底层外衣,看透它的运行机制与破局之策。
一、 什么是依赖预构建?
概念: 依赖预构建是指 Vite 在开发阶段启动时,对第三方 node_modules 包进行一次"预加工"的过程。也就是利用基于 Go 语言编写的 esbuild 将第三方依赖文件转化为符合浏览器规范的 ESM 格式并进行打包合并。
核心作用:它解决了什么问题?
- 格式统一(兼容 CommonJS / UMD) :浏览器原生的 ESM 无法识别 CommonJS 或 UMD 格式的包。像 React 等依然采用传统格式的古董包,如果直接扔给浏览器会直接报错。预构建能将它们统一转化为标准 ESM 格式,解决了非 ESM 包无法被浏览器识别的问题。
- 减少 HTTP 请求数(提升页面加载速度) :部分 ESM 包(如
lodash-es)内部极其零碎,包含几百个小文件。如果直接在浏览器中加载,会触发几百个并发 HTTP 请求,直接卡死浏览器。预构建将这些相关依赖文件合并成一个或几个特定的 ESM 文件,突破了浏览器的网络并发限制,大幅提升了开发环境的页面加载速度。
二、 依赖预构建的四大核心流程
Vite 的依赖预构建是一个严密的流水线,整体流程分为四步:
scss
[服务器启动] ──> 1. 缓存判断 (有效则直读)
(缓存失效) │
2. 依赖扫描 (esbuild 虚假构建)
│
3. 依赖打包 (CJS转ESM/模块合并)
│
4. 信息写盘 (_metadata.json)
1. 缓存判断(Cache Validation)
在服务器启动的一瞬间,Vite 首先会检查之前的预构建成果是否依然有效,以避免重复操作:
- Vite 会查看
node_modules/.vite文件夹是否存在,以及其中的_metadata.json元数据文件。 - Vite 会根据
package.json的 dependencies 依赖、包管理器的锁定文件(如package-lock.json)以及vite.config.ts中的相关配置,计算出一个 Hash 值。 - 如果 Hash 值未变,Vite 直接跳过后续步骤,从本地磁盘读取缓存,实现秒级启动。
2. 依赖扫描(Dependency Scanning)
如果缓存失效或不存在,Vite 必须找出代码中到底用到了哪些第三方包,确定出一份精确的依赖清单(例如:vue, axios, lodash-es):
- Vite 会从
index.html入口开始,递归扫描所有的源代码(JS/TS/Vue/JSX)。 - 扫描过程中,Vite 使用 esbuild 作为扫描器 ,根据代码中的
import语句以及包含在optimizeDeps.include中的项,快速进行一次"虚假构建"。这次构建不生成最终代码,只为了捞出所有裸导入(Bare Imports)的依赖名称。
3. 依赖打包(Dependency Bundling)
这是预编译中最核心的一步。Vite 会调用 esbuild 对依赖清单进行格式转换和模块合并:
- 极致速度 :得益于 Go 语言编写的 esbuild,这一步比传统的 Webpack 快 10 到 100 倍。
- 格式转换:将 CommonJS 或 UMD 格式的包(如 React)转换为标准 ESM 格式,使浏览器能直接识别。
- 模块合并 :将
lodash-es这种内部有几百个小文件的包,打包成一个或几个特定的 ESM 文件,突破浏览器并发请求限制。
4. 构建信息写入磁盘(Writing Metadata)
最后一步是将打包后的依赖存入 node_modules/.vite/deps 目录下,方便下次对比和加载,并更新元数据:
- 元数据文件 :更新
_metadata.json文件。这个文件记录了文件的 Hash 值、源代码中的原始导入路径、以及预构建后的文件路径的映射关系。 - 路径重写 :当服务器运行时,Vite 会拦截浏览器的请求,根据这份元数据将路径重写为指向
.vite/deps缓存文件的路径。
💡 主动触发与强制刷新:
- 自动触发 :开发中如果修改了
package.json的依赖或optimizeDeps配置,Vite 会自动触发重新预构建。- 手动强制触发 :如果需要手动强制重新预构建,可以删除
node_modules/.vite目录,或执行vite optimize命令(也可以在启动时执行vite --force)。
三、 深度拷问:为什么开发用 esbuild,生产用 Rollup?
这是大厂前端面试高频出现的经典架构题。Vite 采用双引擎架构(esbuild + Rollup),背后有着深层的工程化考量。
1. Vite 开发环境选择 esbuild 的原因
Vite 在开发环境选择 esbuild 主要是看中了其绝对的速度优势和开发效率:
- 编译速度极快:esbuild 是使用 Go 语言开发的,直接编译为机器码,执行效率比 JS 编写的构建工具快很多,通常快 10-100 倍。
- 完美支持 ESM 转换 :esbuild 可将 CommonJS、UMD 直接转化为 ESM,适合浏览器按需加载。而 Rollup 对 CommonJS 还需要使用插件
@rollup/plugin-commonjs,配置比较复杂,还会进一步降低速度。 - 架构轻量化:esbuild 在这里仅关注依赖的转换,而不像 Rollup 需要完整的依赖构建图。
- 极致的文件快译与低延迟 HMR:Vite 在开发环境基于原生 ES Modules 进行模块热更新(只更新被修改模块及其依赖,更新延迟低)。在这个过程中,esbuild 仅负责单个文件的快速转译(如 TS 转 JS),不涉及复杂的打包和全量依赖图重构。而相比之下,Rollup 需基于完整依赖图重新打包,热更新耗时久,并不适合高频更新的开发场景。
2. Vite 生产环境使用 Rollup 打包原因
既然 esbuild 这么快,为什么 Vite 生产环境依然选择 Rollup 打包?主要原因是生产环境构建需要更多的优化和兼容性支持。Rollup 在长期的工程化沉淀中拥有明显的应用优势:
- 更强大的产物优化 :Rollup 在生产环境下的 Tree-shaking(树摇) 、代码分割(Code Splitting)、代码合并的能力是远优于 esbuild 的,能生成体积较小的精简 bundle。
- 灵活的拆包策略 :Rollup 支持
manualChunks和动态导入的优化策略,可极其灵活地拆分大型应用,减少首屏加载时间。 - 庞大完善的插件生态:Rollup 的插件生态非常丰富,可以支持多种类型资源的处理(JS、CSS、图片、SVG、字体资源打包、打包产物压缩等),而 esbuild 的插件生态和定制化能力目前还较为年轻。
- 稳健的浏览器兼容性:Rollup 对浏览器兼容性较好,能配合构建链轻松生成兼容旧版浏览器的代码,确保线上产物的绝对稳定性。
📌 总结
Vite 的双引擎架构是前端工程化的一种精妙平衡:在开发阶段,它追求极致的速度,因此用 esbuild 大刀阔斧地做快速转换;在生产环境,它追求最终产物的体积与兼容性,因此用 Rollup 慢工出细活。 这种"两头通吃"的策略,才成就了如今 Vite 无法撼动的统治地位。