Vite 到底在项目里干了什么?从 dev 到 build 全链路解析
你每天都在用 Vite,但你知道 npm run dev 背后发生了什么吗?
前两年用 Webpack 的时候,改一行代码等 5 秒热更新是家常便饭。后来换了 Vite,嚯,改完代码浏览器瞬间就更新了。我当时的反应是:「这也太快了吧,它是不是偷偷跳过了什么步骤?」带着这个疑问,我把 Vite 的工作原理翻了个底朝天。
Vite 不是魔法,它的快是有道理的。这篇文章从开发服务器、HMR、依赖预构建、到生产构建,拆解 Vite 在项目中的每一步操作。看完你就知道,为什么 Vite 能做到「毫秒级热更新」,以及它在生产构建中做了哪些你看不见的优化。
1. 开发服务器:不打包,直接跑
传统工具(Webpack)在开发时会把所有模块打包成一个或几个 bundle,项目越大,启动越慢。Vite 完全换了一个思路:不打包。
基于原生 ESM 的按需加载
现代浏览器原生支持 ES Modules。Vite 的开发服务器利用了这一点——它启动一个 HTTP 服务器,当浏览器请求某个模块时,Vite 才去处理和转换那个文件,然后直接返回。
// 你写的代码
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
// 浏览器实际请求的(Vite 转换后)
import { ref } from '/node_modules/.vite/deps/vue.js?v=abc123'
import MyComponent from '/src/MyComponent.vue?t=1710000000'
原理揭秘:为什么启动快?
Webpack 启动时需要从入口文件出发,递归解析所有依赖,构建完整的模块依赖图,然后打包输出。项目有 1000 个模块,就要处理 1000 个模块。
Vite 启动时只需要启动 HTTP 服务器和预构建第三方依赖(下面会讲)。你的业务代码一个都不碰。当你在浏览器里打开页面,浏览器发现 <script type="module">,开始请求入口模块,Vite 才按需处理。如果某个页面只用了 50 个模块,那 Vite 就只处理这 50 个。
2. 依赖预构建:第三方库的特殊处理
虽然 Vite 开发时不打包业务代码,但对 node_modules 里的第三方库会做一个「预构建」处理。这一步由 esbuild 完成(速度极快)。
为什么需要预构建?
两个原因:
// 问题 1:CJS → ESM 转换 // lodash-es 是 ESM,没问题 // 但 lodash 是 CommonJS (module.exports),浏览器不认识 // → esbuild 把 CJS 转成 ESM // 问题 2:请求瀑布(request waterfall) // lodash-es 包含 600+ 个小模块文件 // 如果浏览器逐个请求,光 HTTP 开销就受不了 // → esbuild 把 lodash-es 打包成一个文件
实际表现
预构建的结果缓存在 node_modules/.vite/deps/ 目录下。你第一次 npm run dev 可能要等几秒(esbuild 在处理依赖),之后再启动就是秒开——因为缓存还在。如果你安装了新依赖或者修改了 vite.config.ts 的 optimizeDeps 配置,Vite 会自动重新预构建。
3. HMR 热更新:为什么改一行代码能秒更新
HMR (Hot Module Replacement) 是开发体验的核心。Vite 的 HMR 基于 ESM,更新速度和项目规模无关——不管你的项目有 100 个文件还是 10000 个文件,改一个组件的更新速度是一样的。
// HMR 更新流程 1. 你修改了 src/components/Button.vue 2. Vite 的文件监听器 (chokidar) 检测到变化 3. Vite 重新编译这 **一个** 文件(通常 < 10ms) 4. 通过 WebSocket 告诉浏览器:「Button.vue 变了」 5. 浏览器请求新版本的 Button.vue 6. Vue 的 HMR runtime 替换组件定义,保留组件状态
为什么比 Webpack HMR 快?
Webpack 的 HMR 需要重新构建受影响的 chunk。如果你改了一个被很多模块导入的工具函数,Webpack 需要重新打包包含这个函数的 chunk 及其依赖链。
Vite 完全不同——它只需要重新编译改动的那个文件,然后让浏览器通过 ESM 的 import 重新请求。更新的「传播范围」由浏览器的模块图天然决定,Vite 自己不需要做打包计算。这就是为什么 Vite 的 HMR 速度和项目规模无关。
Vue SFC 的精细化 HMR
@vitejs/plugin-vue 对 .vue 文件做了特殊处理。它会把 SFC 的 <template>、<script>、<style> 拆分成独立的"虚拟模块"。如果你只改了样式,只有 style 模块会更新,template 和 script 完全不动。
// 一个 .vue 文件会被拆分为: /src/App.vue → script 部分 /src/App.vue?vue&type=template → template 部分 /src/App.vue?vue&type=style → style 部分 // 你只改了 CSS?只重新请求 style 虚拟模块 // 你只改了模板?只重新请求 template 虚拟模块 // 组件状态(ref 数据)保持不变
4. 生产构建:开发时不打包,上线时打得很彻底
开发时 Vite 依赖浏览器的 ESM 加载,但生产环境不能这样做——成百上千个 HTTP 请求会严重影响加载性能。所以 vite build 会用 Rollup(Vite 6+/Vite 7 开始迁移至 Rolldown)做完整打包。
Vite 默认做了哪些优化?
vite build 默认开启的优化: 1. Tree Shaking → 删除没用到的代码(死代码消除) 2. Code Splitting → 按路由自动拆分 chunk 3. CSS 提取 → 把 CSS 从 JS 中抽离成独立文件 4. 资源内联 → 小于 4KB 的图片自动转 base64 5. 预加载指令 → 自动注入 <link rel="modulepreload"> 6. 压缩 → 默认用 esbuild 压缩 JS,速度极快 7. 哈希文件名 → assets/index-[hash].js 实现长期缓存
原理揭秘:自动代码分割
当你在 Vue Router 中使用 () => import('./pages/About.vue') 时,Rollup 会自动把 About 页面拆分成独立的 chunk。更智能的是,如果 About 和 Contact 两个页面都引用了同一个大型组件库(比如 echarts),Rollup 会自动把共享部分提取成一个单独的 chunk,避免重复加载。
Vite 还可以通过 build.rollupOptions.output.manualChunks 让你手动控制分包策略,比如把所有 vendor 代码打进一个 chunk 来利用浏览器缓存。
5. 插件系统:Vite 的可扩展架构
Vite 的插件 API 兼容 Rollup 插件接口,同时扩展了一些 Vite 特有的钩子。这意味着大量现成的 Rollup 插件可以直接在 Vite 中使用。
// Vite 插件的核心钩子
export default function myPlugin() {
return {
name: 'my-plugin',
// Vite 特有:配置开发服务器
configureServer(server) { ... },
// Rollup 兼容:转换模块内容
transform(code, id) {
// 比如:自动把 .svg 转成 Vue 组件
if (id.endsWith('.svg')) {
return `export default \`${code}\``
}
},
// Rollup 兼容:自定义模块解析
resolveId(source) { ... },
// Rollup 兼容:加载虚拟模块
load(id) { ... },
}
}
你每天用的 @vitejs/plugin-vue 就是利用了 transform 钩子,把 .vue 文件编译成浏览器能执行的 JavaScript。UnoCSS 的 Vite 插件则利用了 transform 和 resolveId/load 来实现按需生成 CSS。
实用提示
在 vite.config.ts 中,插件的顺序很重要。Vite 会按数组顺序依次调用 transform。如果你遇到某个插件不生效,检查一下它是不是被排在了另一个插件后面导致代码已经被先一步转换了。可以用 enforce: 'pre' 或 enforce: 'post' 来控制执行时机。
总结一张图
┌─────────────────────────────────────────────────┐ │ npm run dev │ ├─────────────────────────────────────────────────┤ │ esbuild 预构建 node_modules → .vite/deps/ │ │ 启动 HTTP 服务器 + WebSocket │ │ 浏览器请求 → Vite 按需编译 → 返回 ESM 模块 │ │ 文件变化 → 编译单文件 → WebSocket 通知 → HMR │ ├─────────────────────────────────────────────────┤ │ npm run build │ ├─────────────────────────────────────────────────┤ │ Rollup / Rolldown 全量打包 │ │ Tree Shaking + Code Splitting + 压缩 │ │ 输出 dist/ 目录,可直接部署 │ └─────────────────────────────────────────────────┘
Vite 的设计哲学是「开发时利用浏览器原生能力做到极致快,生产时用成熟的打包工具做到极致小」。理解了这个核心思路,你就理解了 Vite 的一切。如果你想进一步了解 Vue3 本身的优化技巧,推荐看看 Vue3 性能优化技巧 和 Vue3 源码解析。