Skip to content
核心概念

核心概念

本章深入讲解 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 请求

预构建的行为

  1. 启动开发服务器时,Vite 自动扫描 node_modules 中的依赖
  2. 用 esbuild 将 CommonJS/UMD 模块转为 ESM
  3. 合并碎片化的内部模块
  4. 结果缓存在 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 依赖且无法迁移。