核心概念
本章深入讲解 Vite 的架构设计和核心原理:为什么它比传统打包工具快一个数量级。
核心理念
Vite 的核心洞察:利用浏览器原生 ES Module(ESM)支持,开发时无需打包,按需编译,生产时用 Rollup 打包优化。
传统打包工具(Webpack/CRA)的工作方式:
开发阶段:打包所有模块 → Bundle → Dev Server → 浏览器
↑ 冷启动随项目变大而线性变慢Vite 的工作方式:
开发阶段:Dev Server → 按需编译单个文件 → 浏览器请求时返回(ESM)
↑ 冷启动几乎恒定,与项目大小无关
生产构建:ESBuild 转换 → Rollup 打包 → 树摇/压缩/分包 → dist/
↑ 追求最优产物体积与加载性能双引擎架构
Vite 内部使用两个引擎,各司其职:
| 阶段 | 引擎 | 职责 | 速度 |
|---|---|---|---|
| 开发 — 依赖预构建 | esbuild | 将 CommonJS/UMD 依赖转为 ESM,合并碎片模块 | 极快(Go 编写) |
| 开发 — 源码编译 | Vite 自身 | 根据请求按需将 TS/JSX/Vue/Sass 等转为浏览器可运行的 ESM | 快(仅编译请求的文件) |
| 生产 — 构建 | Rollup | 打包、Tree Shaking、代码分割、压缩 | 合理(追求产物最优) |
🔬 深入原理:之所以不让 esbuild 负责生产构建,是因为 Rollup 的插件生态超级成熟,且 Rollup 在代码分割和 Tree Shaking 方面更精细。esbuild 虽然快,但在某些构建优化上不如 Rollup。
开发服务器:原生 ESM 按需编译
传统打包工具(Webpack)
浏览器请求 → Webpack Dev Server → 返回完整 Bundle → 浏览器解析执行
每次请求都要先打包。项目越大,打包越慢。
Vite
浏览器请求 → Vite Dev Server → 编译单文件为 ESM → 浏览器直接执行
以一个实际的网络请求瀑布流为例:
浏览器请求 / → index.html
浏览器请求 /src/main.js → Vite 编译 main.js → 返回 ESM
浏览器请求 /src/App.jsx → Vite 编译 JSX → 返回 ESM
浏览器请求 /src/components/Header.jsx → Vite 编译 JSX → 返回 ESM
...每个文件独立编译,只有当浏览器真正请求时才处理该文件。这意味着:
- 冷启动速度几乎恒定,与模块数量无关
- 更新速度只编译变更的单个文件(O(1) 复杂度)
⚡ 性能提示:Vite 开发服务器首次请求某个文件时才编译它。这就是为什么大项目的冷启动依然很快——它不"预编译"全部源码,只编译被请求的文件。
依赖预构建
Vite 在开发阶段的另一个关键优化:依赖预构建(Dependency Pre-Bundling)。
为什么需要预构建?
问题一:很多 npm 包是 CommonJS 格式,浏览器无法直接执行。
// node_modules/lodash/index.js(CommonJS)
module.exports = { debounce: function() {...} }
// ❌ 浏览器不认识 module.exports
Vite 用 esbuild 将它们转为 ESM:
// 预构建产物(ESM)
export const debounce = function() {...}
// ✅ 浏览器可以直接执行
问题二:某些包有大量内部模块,如果不合并会产生数百个 HTTP 请求。
lodash 有 600+ 个独立 .js 文件
→ 预构建合并成一个 lodash.js
→ 浏览器只需要 1 个 HTTP 请求预构建的行为
- 启动开发服务器时,Vite 自动扫描
node_modules中的依赖 - 用 esbuild 将 CommonJS/UMD 模块转为 ESM
- 合并碎片化的内部模块
- 结果缓存在
node_modules/.vite/目录
# 手动触发预构建
npx vite --force # 重新执行预构建,忽略缓存
# 或删除缓存目录
rm -rf node_modules/.vite配置预构建
// vite.config.js
export default defineConfig({
optimizeDeps: {
// 强制预构建某些依赖(Vite 自动扫描不到的)
include: ['lodash-es', 'deep-pkg/deep-file'],
// 排除某些不需要预构建的依赖
exclude: ['your-esm-pkg'],
// esbuild 选项
esbuildOptions: {
target: 'es2020',
},
},
})| 选项 | 说明 |
|---|---|
include |
强制预构建列表。动态 import 或不在源码中被 import 的依赖需要加到这里 |
exclude |
排除列表。已经是纯 ESM 且不想被合并的包加到这里 |
entries |
自定义扫描入口(默认扫描 index.html) |
esbuildOptions |
透传给 esbuild 的选项 |
🚨 陷阱:动态 import 的模块(
import('./pkg'))不会被自动预构建,需要手动加到optimizeDeps.include中,否则首次加载时可能报错。
缓存系统
Vite 有两层缓存:
| 缓存层 | 位置 | 内容 | 失效条件 |
|---|---|---|---|
| 文件系统缓存 | node_modules/.vite/ |
预构建的依赖 | --force 参数 / 删除 .vite 目录 / 依赖变更 |
| 浏览器缓存 | HTTP 304 / 强缓存 | 编译后的源码模块 | 关闭 dev server / 源码修改(304 协商缓存) |
依赖(node_modules):
变更频率低 → 强缓存(Cache-Control: max-age=31536000, immutable)
通过 URL 的版本哈希管理失效(依赖版本不变的请求 URL 不变)
源码(src/**):
变更频率高 → 304 协商缓存(If-None-Match / ETag)
修改源码后下次请求自动更新🔬 深入原理:Vite 给预构建的依赖 URL 加上版本查询参数(如
?v=abc123),依赖内容变化时哈希变化,URL 变化,浏览器自动请求新文件。这就是为什么依赖更新后不需要手动清除浏览器缓存。
模块解析
Vite 的模块解析遵循以下顺序,并做了增强:
| 导入形式 | 解析方式 |
|---|---|
import foo from 'foo' |
node_modules 查找(标准 Node 解析) |
import foo from './foo' |
相对路径解析 |
import foo from '@/components/foo' |
路径别名(需配置 resolve.alias) |
import foo from '/src/foo.js' |
绝对路径(从项目根目录开始) |
import data from './data.json' |
JSON 文件导入(直接导出对象) |
import styles from './App.module.css' |
CSS Modules |
import url from './img.png' |
静态资源 → 返回 URL 字符串 |
与 Webpack 的架构对比
| 维度 | Vite | Webpack |
|---|---|---|
| 开发阶段策略 | 原生 ESM,按需编译 | 全量打包 |
| 冷启动 | 极快(预构建依赖 + 按需编译源码) | 慢(先打包再启动) |
| HMR | 极快(O(1) 复杂度,只更新变更模块) | 随项目增大变慢 |
| 依赖编译 | esbuild(Go,快 10-100 倍) | babel-loader / ts-loader(JS,较慢) |
| 生产构建 | Rollup | Webpack(TerserPlugin) |
| 配置 | 简洁,约定大于配置 | 灵活但繁杂 |
| 插件 API | 兼容 Rollup 插件 | 自有的 Tapable 体系 |
| 代码分割 | 自动(基于 Rollup) | 手动配置 splitChunks / 动态 import |
💡 最佳实践:新项目优先选 Vite,除非你有大量 Webpack 特定的 loader/plugin 依赖且无法迁移。