文章

Vite原理深度解析:基于ESM的下一代构建引擎

Vite 开发时利用浏览器原生 ESM 实现按需加载不打包,用 esbuild 预构建依赖,用 Rollup 做生产打包,本质是用两套策略分别解决体验与体积。 面试常考它与 Webpack 的根本差异、依赖预构建为什么用 esbuild、以及 HMR 为何如此快。

Vite原理深度解析:基于ESM的下一代构建引擎

一句话概括

Vite 是一款”开发时不打包、生产时深度优化”的前端构建工具,利用浏览器原生 ESM 实现按需加载,用 esbuild 做预构建,用 Rollup 做生产打包——本质上是用两套策略分别解决开发体验和产物体积问题。

核心知识点

1. 开发不打包:利用浏览器原生 ESM

传统打包工具(Webpack)在开发阶段也要递归构建整个模块依赖图。Vite 直接输出原生 ESM 给浏览器,浏览器自己发 HTTP 请求加载模块:

1
2
3
4
5
6
7
8
# Vite 冷启动只需两步
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev

# 浏览器 Network 面板会看到每个模块都是独立请求
# /src/main.tsx → 200B
# /node_modules/.vite/deps/react.js → 预构建后的 ESM
# /src/App.tsx → 800B

关键差异:Webpack 冷启动 3-5 分钟的大型项目,Vite 只需 3-5 秒。因为 Vite 只启动 Koa 服务 + 预构建 node_modules,不递归构建整个模块图。

2. 预构建(Pre-bundling):为什么第三方库还是要打包

浏览器能跑 ESM,但 node_modules 里的第三方库有两种问题:

  • CJS → ESM 转换:大量 npm 包仍是 module.exports,浏览器不认识
  • 模块碎片:lodash-es 有 500+ 个文件,浏览器直接加载要发 500+ 个 HTTP 请求(TCP 握手开销巨大)

Vite 用 esbuild(Go 编写,比 JS 快 10-100 倍)做预构建,把 CJS 转 ESM 并将碎片模块合并成一个文件:

1
2
3
4
5
6
7
// vite.config.ts
export default defineConfig({
  optimizeDeps: {
    include: ['lodash-es', 'moment'],   // 强制预构建(动态导入也能被覆盖)
    exclude: ['some-esm-only-lib'],      // 跳过已支持 ESM 的库
  },
});

预构建结果缓存在 node_modules/.vite/deps/,通过 package-lock.json 的哈希判断是否需要重新构建。

3. HMR:只推送变更模块,不重建依赖图

Vite 的 HMR 比 Webpack 快的关键:不需要重新构建依赖图。文件变更 → chokidar 检测 → WebSocket 推送变更文件路径 → 浏览器用 ESM 重新 import 该模块:

1
2
3
4
5
6
7
8
9
10
11
12
// Vite HMR 客户端(浏览器中运行的精简逻辑)
const ws = new WebSocket(`ws://${location.host}`);

ws.onmessage = async ({ data }) => {
  const { type, updates } = JSON.parse(data);
  if (type === 'update') {
    for (const u of updates) {
      await import(`${u.path}?t=${u.timestamp}`);  // ESM 重新加载变更模块
      import.meta.hot?.accept();                     // 触发模块自更新
    }
  }
};

核心差异:Webpack HMR 需要遍历模块依赖关系 → 重新构建 Chunk → 通过运行时替换;Vite 只需要一行 import()。

4. 生产构建:为什么用 Rollup 而不是 esbuild

虽然 esbuild 极快,但生产构建选择 Rollup 的原因:

维度esbuildRollup
Tree Shaking基础支持✅ AST 级别精确消除
代码分割有限✅ 原生支持,配置丰富
插件生态小✅ 成熟(rollup-plugin-*)
输出格式ESM/CJS✅ UMD/IIFE/System 全覆盖

简单说:esbuild 快但不精,Rollup 慢但功能全面。生产打包”一次慢,全网快”,深度优化更值得。

5. 路径重写:裸模块 → 可请求 URL

Vite 将源码中的裸模块导入(import 'vue')重写为服务器可识别的路径:

1
2
3
4
5
6
7
// 源码
import { ref } from 'vue'
import { debounce } from 'lodash-es'

// Vite 重写后
import { ref } from '/node_modules/.vite/deps/vue.js?v=f4234d2f'
import { debounce } from '/node_modules/.vite/deps/lodash-es.js?v=a1b2c3d'

这是 Vite 开发服务器的核心中间件逻辑——拦截所有 .js/.ts 请求,用 esbuild 即时编译,同时替换 import 路径。

其实你每天都在用

  • npm create vite@latest 3 秒建项目:背后是 Vite 省略了整个打包步骤,直接启动 Koa 开发服务器
  • 改一行 CSS 毫秒级热更新:Vite 只推送这一个 CSS 文件,浏览器用 import.meta.hot 替换样式,不需要重建整个 Chunk
  • import.meta.env.VITE_API_URL:Vite 在编译时用 define 替换环境变量,不是运行时注入
  • .vue 文件的 scoped 样式:@vitejs/plugin-vue 在编译时给每个选择器加 data-v-xxxx 属性,实现样式隔离
  • Vite 的 ?raw / ?url 后缀导入:import str from './file?raw' 直接把文件内容作为字符串导入,Vite 内置支持的资源处理

常见误解(FAQ)

❌ 误区 1:「Vite 完全不打包」

Vite 开发时不打包你的源码,但第三方依赖仍然预构建。node_modules 里的 CJS 包、碎片化的 ESM 包都会被 esbuild 合并转换。完全不打包的话,引入 lodash-es 会让浏览器发 500+ 个请求。

❌ 误区 2:「Vite 比 Webpack 快只是因为 esbuild」

esbuild 贡献了预构建的速度,但更大的差异来自架构:Webpack 开发时也要递归构建整个模块图,Vite 根 本不构建(浏览器自己加载)。esbuild 是加速器,架构决策才是根本原因。

❌ 误区 3:「Vite 生产构建也用 esbuild」

生产构建用的是 Rollup。esbuild 的 Tree Shaking 和代码分割能力不如 Rollup 精细。对生产环境来说,打包质量比打包速度更重要。

❌ 误区 4:「optimizeDeps.include 加上所有依赖就行」

过度预构建会导致:首次启动变慢(需要打包更多依赖)、缓存命中率降低、而且某些支持 ESM 的现代包(如 vue 本身)已经被 Vite 自动处理,不需要手动加。

一句话总结

Vite 的成功不在于”比 Webpack 更快”,而在于敢于推翻”开发也必须打包”的惯性思维——信任浏览器,把计算分摊到客户端,让开发体验回归秒级响应。

本文由作者按照 CC BY 4.0 进行授权