什么是预构建,Vite中如何使用预构建
Vite 的预构建(Pre-Bundling)是指开发服务器启动时对第三方依赖进行的预处理过程,这是 Vite 的核心优化策略之一。Vite 相比于传统的 Webpack,能做到开发时的模块按需编译,而不用先打包完再加载。
需要注意的是,我们所说的模块代码其实分为两部分,一部分是源代码,也就是业务代码,另一部分是第三方依赖的代码,即node_modules
中的代码。所谓的no-bundle
只是对于源代码而言,对于第三方依赖而言,Vite 还是选择 bundle(打包),并且使用速度极快的打包器 Esbuild 来完成这一过程,达到秒级的依赖编译速度。
接下里我们将深入的探讨 Vite 预构建,《官方文档地址》
🧠 一、扩展与解释
本文开头第二段说到Vite的“no-bundle仅针对源代码,第三方依赖仍需打包”,为了方便大家理解这一设计理念,下面我会结合其核心目标(极速开发体验)和技术实现机制来进行分层次解读:
1. 核心概念:什么是“no-bundle”?
- 源代码(业务代码):开发者编写的文件(如
.vue
、.jsx
、.ts
、.css
等)。 Vite在开发模式下不打包这些文件,而是直接通过浏览器原生ESM加载,实现按需编译(即浏览器请求哪个文件,Vite实时转换后返回该文件)。 - 第三方依赖:
node_modules
中的库(如react
、lodash-es
)。 Vite使用Esbuild预打包这些依赖,生成单文件或少量分块(如react
→react.js
),再交给浏览器加载。
2. 为什么第三方依赖必须打包?
(1) 解决请求瀑布问题
某些库(如lodash-es
)采用多文件模块设计(内部含数百个文件)。若直接按ESM加载,浏览器会同时发起数百个请求,导致网络阻塞和加载延迟。 ✅ 解决方案:Esbuild将其打包为单文件,将数百请求合并为1个。下图中加载时会发出特别多的请求,导致页面加载的前几秒几都乎处于卡顿状态。
然而在依赖的预构建后 ,lodash-es
这个库的代码被打包成了一个文件,这样请求的数量会骤然减少,页面加载也快了许多。下图为对比:
(2) 兼容非ESM模块
部分老库使用CommonJS(如require()
语法),而浏览器仅支持ESM。Vite需将其转换为ESM格式才能运行。 ✅ 解决方案:Esbuild统一转换CJS/UMD依赖为ESM。
(3) 性能优化
依赖代码通常稳定不变,预打包后可缓存复用,避免每次启动重复编译。Esbuild用Go编写,比JS工具快10-100倍,实现“秒级依赖编译”。
3. Esbuild的核心优势
- 语言级性能:基于Go的并行处理能力,充分利用多核CPU,远超JS工具链(如Webpack)。
- 单一任务专注:仅负责依赖打包(不参与业务代码编译),与Vite的按需编译形成分工协作。
- 缓存机制:依赖包内容变化时,通过
package-lock.json
等锁文件校验,仅增量更新变动的依赖。
4. Vite的分层处理架构
阶段 | 源代码(业务代码) | 第三方依赖(node_modules ) |
---|---|---|
开发模式 | 按需编译(no-bundle) | Esbuild预打包(bundle) |
生产模式 | Rollup打包(传统bundle) | Rollup打包(与源码合并) |
核心目标 | 快速启动、热更新(HMR) | 减少请求、语法兼容、长期缓存 |
💡 关键差异:开发环境中,业务代码的“no-bundle”是动态编译(浏览器驱动),而依赖的“bundle”是静态预生成(启动时一次性完成)。
⚙️ 二、预构建的核心机制 (自动开启)
-
自动触发条件:
- 首次启动开发服务器(
vite dev
)时,自动预构建依赖。 - 检测到
package.json
变更(如新增依赖)时,重新预构建。 - 手动强制重建(通过
--force
或删除缓存)。
- 首次启动开发服务器(
-
缓存位置: 预构建结果缓存于
node_modules/.vite
目录,文件名为_metadata.json
和哈希化的 ES 模块文件。
🔧 三、手动配置预构建 (手动开启)
在 vite.config.js
中通过 optimizeDeps
配置项自定义行为:
// vite.config.js
export default defineConfig({optimizeDeps: {// 1. 自定义预构建入口(适用于非主流模块格式)entries: ['./src/my-custom-entry.js']// 2. 显式指定需预构建的依赖(默认自动扫描)include: ['lodash', 'axios'],// 3. 排除不需要预构建的依赖exclude: ['moment'], // 4. 强制忽略缓存(每次启动都重新构建)force: true,}
})
1. entries —— 指定额外入口文件(如动态导入的模块)。
当默认扫描 HTML 文件的行为无法满足需求的时候,比如项目入口为vue
格式文件时,你可以通过 entries 参数来配置:
// vite.config.ts
{optimizeDeps: {// 为一个字符串数组entries: ["./src/main.vue"];}
}
如果要使用 glob 语法配置多入口,需要修改 build.rollupOptions.input 配置。(entries 配置也支持 glob 语法 )
安装 glob 依赖:
npm install fast-glob
vite.config.ts 配置:
// ... existing code ...
import { resolve } from "path";
import glob from 'fast-glob';// ... existing code ...build: {minify: "esbuild",assetsDir: "assets",outDir: `dist`,brotliSize: false,rollupOptions: {input: {main: resolve(__dirname, 'src/main.ts'),// 使用 glob 语法匹配多个入口文件(示例)// html: glob.sync('src/**/*.html'),// js: glob.sync('src/**/*.{js,ts}')}}},
// ... existing code ...
如果需要使用 glob 语法配置多个入口点,可以取消注释示例部分并根据项目结构调整 glob 模式。例如:
src/**/*.html
匹配所有 HTML 文件src/**/*.{js,ts}
匹配所有 JS/TS 文件src/views/*.vue
匹配 views 目录下的 Vue 文件
2. include —— 强制预构建未自动识别的依赖(如 CJS 包)。
3. exclude —— 避免对某些依赖预构建(如已预构建的库)。
4. force —— 开发服务器启动时强制重新预构建(等价于命令行 vite --force
)。
🛠️ 四、命令行强制预构建
启动开发服务器时添加 --force
参数:
vite dev --force # 或 vite --force
此命令会忽略缓存,重新执行预构建流程。
⚡ 五、高级场景优化
1. 解决 CommonJS 依赖问题
若控制台警告 ✘ [WARNING] CommonJS module ...
,表示该依赖未预构建。手动添加到 include
:
optimizeDeps: {include: ['commonjs-package'] // 添加警告中的包名
}
2. 加速 Monorepo 项目
在 Monorepo 中显式声明子包依赖:
optimizeDeps: {include: ['@monorepo/ui-components', '@monorepo/utils']
}
3. 禁用预构建(不推荐)
optimizeDeps: {disabled: true // 仅调试用,严重降低性能!
}
❌ 六、常见问题处理
问题现象 | 解决方案 |
---|---|
依赖更新后未重新预构建 | 运行 vite --force 或删除 .vite 目录 |
预构建后动态导入报错 | 检查 optimizeDeps.include 是否包含该依赖 |
预构建耗时过长 | 排除大体积库(如 exclude: ['three'] ) |
后续遇到问题会继续补充!