跳到主要内容

Vite 原理与优势

问题

Vite 是什么?它为什么比 Webpack 快?原理是什么?

答案

Vite 是由尤雨溪创建的下一代前端构建工具,名字来源于法语"快"(vite)。它的核心设计理念是 利用浏览器原生 ES Module(ESM)能力,在开发阶段跳过打包步骤,实现极速的冷启动和热更新。

一句话总结

Vite = 开发时 No-Bundle(原生 ESM + esbuild 预构建) + 生产时 Rollup 打包

开发模式原理:No-Bundle

传统打包器的问题

Webpack 等传统打包器在开发时需要先将所有模块 打包成 bundle 才能启动开发服务器。随着项目规模增长,冷启动时间从几秒增长到几十秒甚至数分钟。

Vite 的解决思路

Vite 将模块分为两类:

  1. 依赖(Dependencies):使用 esbuild 预构建,速度极快(Go 编写,比 JavaScript 打包器快 10-100 倍)
  2. 源码(Source Code):利用浏览器原生 ESM,按需编译,无需打包

浏览器发起 import 请求时,Vite 的开发服务器拦截请求,按需编译对应模块并返回。这意味着:

  • 冷启动极快:不需要分析整个模块图、打包所有文件
  • 按需加载:只编译当前页面用到的模块
  • 编译速度快:单文件编译,利用 esbuild 转译 TypeScript/JSX
浏览器中的 ESM 请求
// 浏览器发出的请求(已被 Vite 处理)
import { createApp } from '/node_modules/.vite/deps/vue.js?v=abc123'
import App from '/src/App.vue?t=1234567890'
import { setupRouter } from '/src/router/index.ts'

// Vite 开发服务器会:
// 1. 拦截每个 import 请求
// 2. 按需编译对应文件(.vue → JS、.ts → JS)
// 3. 返回浏览器可执行的 ESM 模块

依赖预构建

为什么需要预构建?

虽然 Vite 基于原生 ESM,但直接使用 node_modules 中的依赖会有两个问题:

  1. 格式不兼容:很多第三方包只提供 CommonJS 格式(如 lodash),浏览器无法直接 import
  2. 请求数量爆炸:有些 ESM 包会产生大量细粒度导入。例如 lodash-es 有 600+ 个子模块,浏览器需要发 600+ 个 HTTP 请求,严重影响性能

esbuild 预构建流程

Vite 在首次启动时,使用 esbuildnode_modules 中的依赖进行预构建:

预构建前后对比
// 预构建前:lodash-es 有 600+ 个文件
import { debounce } from 'lodash-es'
// 浏览器需要请求:lodash-es/debounce.js → lodash-es/_debounce.js → ...(瀑布请求)

// 预构建后:esbuild 将 lodash-es 合并为单个文件
import { debounce } from '/node_modules/.vite/deps/lodash-es.js?v=abc123'
// 只需要一个请求!
预构建缓存机制

预构建结果缓存在 node_modules/.vite 目录。只有当以下内容变化时才会重新预构建:

  • package.json 中的 dependencies 列表
  • 包管理器的 lock 文件(package-lock.jsonpnpm-lock.yaml 等)
  • vite.config.ts 中的 optimizeDeps 相关配置

HMR 原理

基于 ESM 的精确 HMR

Vite 的 HMR 是在 原生 ESM 之上实现的。当一个模块发生变化时,Vite 只需精确地使已修改的模块与其最近的 HMR 边界之间的链路失效,而不需要重新构建整个 bundle。

HMR API 示例
// Vite 提供的 HMR API
if (import.meta.hot) {
// 接受自身更新
import.meta.hot.accept((newModule) => {
// 用新模块替换旧模块
console.log('模块已更新', newModule)
})

// 接受依赖更新
import.meta.hot.accept('./module.ts', (newModule) => {
// 依赖模块更新后的回调
})

// 清理副作用
import.meta.hot.dispose(() => {
// 模块被替换前执行清理
clearInterval(timer)
})
}

Vite HMR vs Webpack HMR

维度Vite HMRWebpack HMR
更新粒度精确到单个模块需要重新编译受影响的 chunk
更新速度与项目大小无关,始终保持快速项目越大越慢
实现方式基于原生 ESM import基于自实现的模块系统
框架集成框架插件自动处理(如 @vitejs/plugin-vue需要 react-hot-loader 等额外配置
面试重点

Vite HMR 的核心优势:更新速度与项目规模无关。无论项目有多大,HMR 都能保持毫秒级响应,因为它只需处理变更的模块本身,而不是整个模块图。

生产构建

为什么不用 esbuild 打包?

虽然 esbuild 编译速度极快,但 Vite 生产环境选择 Rollup 打包,原因如下:

  1. 代码分割(Code Splitting):esbuild 的代码分割能力不够成熟,无法生成最优的 chunk 分割策略
  2. CSS 处理:esbuild 对 CSS 代码分割的支持有限
  3. 插件生态:Rollup 拥有成熟的插件生态和灵活的插件 API
  4. 产物优化:Rollup 对 Tree Shaking 的支持更加彻底,能生成更小的 bundle
  5. 稳定性:Rollup 经过多年验证,产物稳定可靠
注意

esbuild 仍然在生产构建中发挥作用——它负责 代码压缩(minify) 步骤,替代 Terser,速度快很多。Vite 4+ 默认使用 esbuild 进行 JS 和 CSS 压缩。

Rollup 打包策略

vite.config.ts - 生产构建配置
import { defineConfig } from 'vite'

export default defineConfig({
build: {
// 使用 Rollup 打包
rollupOptions: {
output: {
// 手动分割 chunk
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-utils': ['lodash-es', 'dayjs'],
},
// 自定义文件名
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},
// 使用 esbuild 压缩(默认),比 terser 快 20-40 倍
minify: 'esbuild',
// chunk 大小警告阈值(KB)
chunkSizeWarningLimit: 500,
// 启用 CSS 代码分割
cssCodeSplit: true,
},
})

Vite vs Webpack 详细对比

维度ViteWebpack
开发冷启动毫秒级(No-Bundle)秒级到分钟级(需全量打包)
HMR 速度毫秒级,与项目大小无关随项目增大而变慢
开发模式原理原生 ESM + 按需编译Bundle + 自实现模块系统
生产构建Rollup自身打包
构建速度快(esbuild 预构建 + 压缩)较慢(JS 编写的 loader/plugin)
配置复杂度开箱即用,约定大于配置配置灵活但复杂
插件生态兼容 Rollup 插件 + Vite 专属丰富的 loader 和 plugin 生态
Tree ShakingRollup 原生支持,效果好支持,但不如 Rollup 彻底
代码分割Rollup 自动分割SplitChunksPlugin
CSS 处理原生支持 PostCSS、CSS Modules需配置 css-loader 等
TypeScriptesbuild 直接编译(仅转译,不类型检查)需 ts-loader 或 babel-loader
静态资源原生 import 支持需 file-loader / asset modules
适用场景新项目首选、中小型到大型项目大型老项目、需要高度定制的场景
浏览器兼容默认支持现代浏览器,可配 @vitejs/plugin-legacy原生支持,通过 Babel 转译
SSR 支持内置实验性 SSR需额外配置

Vite 核心配置示例

vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

// https://cn.vite.dev/config/
export default defineConfig({
// 插件配置
plugins: [
vue(),
],

// 路径解析
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
},
// 导入时可省略的扩展名
extensions: ['.ts', '.tsx', '.js', '.jsx', '.vue', '.json'],
},

// 开发服务器配置
server: {
port: 3000,
open: true,
// 代理配置(解决开发环境跨域)
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/api/, ''),
},
},
},

// 构建配置
build: {
target: 'es2015',
outDir: 'dist',
sourcemap: false,
rollupOptions: {
output: {
manualChunks: (id: string) => {
// 将 node_modules 中的依赖分割为独立 chunk
if (id.includes('node_modules')) {
return 'vendor'
}
},
},
},
},

// CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`,
},
},
modules: {
localsConvention: 'camelCase',
},
},

// 环境变量前缀
envPrefix: 'VITE_',
})

安装 Vite

npm install vite -D

Vite 插件机制

Vite 的插件机制基于 Rollup 的插件接口进行了扩展,分为 通用钩子(兼容 Rollup)和 Vite 专属钩子 两类。

插件执行时机

  • 绿色节点为 Vite 专属钩子
  • 蓝色节点为 兼容 Rollup 的通用钩子

自定义插件示例

plugins/vite-plugin-custom.ts
import type { Plugin } from 'vite'

export function customPlugin(): Plugin {
return {
// 插件名称
name: 'vite-plugin-custom',

// Vite 专属钩子:修改配置
config(config, { mode }) {
if (mode === 'production') {
return {
build: { sourcemap: true },
}
}
},

// Vite 专属钩子:配置开发服务器
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 自定义中间件逻辑
next()
})
},

// 通用钩子(兼容 Rollup):解析模块 ID
resolveId(source: string) {
if (source === 'virtual:my-module') {
return '\0virtual:my-module'
}
},

// 通用钩子:加载模块内容
load(id: string) {
if (id === '\0virtual:my-module') {
return `export const msg = "Hello from virtual module!"`
}
},

// 通用钩子:转换模块代码
transform(code: string, id: string) {
if (id.endsWith('.ts')) {
// 对 TypeScript 文件进行自定义转换
return {
code: code.replace(/__TIMESTAMP__/g, Date.now().toString()),
map: null,
}
}
},

// Vite 专属钩子:自定义 HMR 处理
handleHotUpdate({ file, server }) {
if (file.endsWith('.custom')) {
server.ws.send({
type: 'custom',
event: 'custom-update',
data: { file },
})
return [] // 阻止默认 HMR
}
},
}
}
Vite 专属钩子一览
钩子说明
config修改 Vite 配置(合并前)
configResolved读取最终配置(合并后)
configureServer配置开发服务器(添加中间件)
configurePreviewServer配置预览服务器
transformIndexHtml转换 index.html
handleHotUpdate自定义 HMR 更新处理

常见面试问题

Q1: Vite 为什么比 Webpack 快?

答案

需要从 开发模式生产构建 两个维度分析:

开发模式(差异最大):

  1. No-Bundle 策略:Webpack 需要先打包所有模块才能启动开发服务器;Vite 直接启动服务器,利用浏览器原生 ESM 按需加载和编译模块,所以冷启动极快
  2. esbuild 预构建:第三方依赖通过 esbuild(Go 编写)预构建,比 JavaScript 编写的打包器快 10-100 倍
  3. 按需编译:只有浏览器实际请求的模块才会被编译,未访问的页面代码不会被处理
  4. HMR 速度:基于 ESM 的 HMR 只需替换变更模块本身,更新速度与项目规模无关

生产构建:

  1. esbuild 压缩:使用 esbuild 进行代码压缩,比 Terser 快 20-40 倍
  2. esbuild 转译:TypeScript/JSX 由 esbuild 转译,跳过类型检查,速度极快
开发启动速度对比(大型项目示意)
// Webpack(需要全量打包)
// 1. 解析所有模块依赖关系(慢)
// 2. 编译所有模块(慢)
// 3. 生成 Bundle(慢)
// 4. 启动 Dev Server
// 总耗时:30s ~ 几分钟

// Vite(No-Bundle + 预构建)
// 1. esbuild 预构建依赖(快,有缓存更快)
// 2. 直接启动 Dev Server
// 3. 浏览器按需请求模块,即时编译
// 总耗时:几百毫秒 ~ 几秒

Q2: Vite 的依赖预构建是什么?为什么需要?

答案

依赖预构建是 Vite 在开发服务器首次启动时,使用 esbuild 对 node_modules 中的第三方依赖进行预处理的过程。

为什么需要预构建? 主要解决两个问题:

1. 格式兼容性——CJS 转 ESM:

CJS 转换示例
// React 只提供 CommonJS 格式
// node_modules/react/index.js
module.exports = { createElement, useState, ... }

// 浏览器无法直接 import CommonJS 模块
// esbuild 预构建后转为 ESM:
// node_modules/.vite/deps/react.js
export { createElement, useState, ... }

2. 请求合并——减少 HTTP 请求数量:

依赖合并示例
// lodash-es 有 600+ 个独立的 ESM 文件
import { debounce } from 'lodash-es'
// 未经预构建:浏览器需要依次请求 debounce.js → _debounce.js → _toNumber.js → ...
// 可能产生 600+ 个 HTTP 请求!

// esbuild 预构建后:将整个 lodash-es 合并为一个文件
// node_modules/.vite/deps/lodash-es.js
// 只需 1 个请求

预构建的缓存策略:

  • 结果缓存在 node_modules/.vite 目录
  • 依赖列表或 lock 文件变化时自动重新构建
  • 可通过 vite --force 强制重新预构建
手动配置预构建
import { defineConfig } from 'vite'

export default defineConfig({
optimizeDeps: {
// 强制预构建某些依赖
include: ['lodash-es', 'axios'],
// 排除某些依赖(已经是合格的 ESM)
exclude: ['my-esm-package'],
},
})

Q3: Vite 生产环境为什么不用 esbuild 打包而用 Rollup?

答案

虽然 esbuild 构建速度极快,但 Vite 在生产构建中选择 Rollup 而非 esbuild,主要有以下原因:

维度esbuildRollup
代码分割支持有限,策略不够灵活成熟的自动和手动代码分割
CSS 代码分割支持不完善完善支持,CSS 与 JS chunk 对应
Tree Shaking基础支持深度 Tree Shaking,效果更彻底
插件生态生态较新成熟丰富的插件生态
产物格式支持有限支持 ESM、CJS、UMD、IIFE 等
产物稳定性仍在快速迭代经过多年生产验证
构建速度极快较慢(但够用)

核心原因总结:

  1. 代码分割不成熟:esbuild 的代码分割无法生成最优的 chunk,可能导致浏览器加载多余代码
  2. CSS 处理能力不足:生产环境需要精确的 CSS 代码分割,确保每个 JS chunk 关联对应的 CSS
  3. 产物优化不够:Rollup 的 Tree Shaking 更加彻底,能生成更小的最终产物
  4. 生态和稳定性:生产环境对稳定性要求高,Rollup 经过更充分的验证
面试加分点

esbuild 并非完全不参与生产构建。Vite 在生产构建中仍然使用 esbuild 来进行 代码压缩(minify),这一步比传统的 Terser 快 20-40 倍。因此 Vite 的生产构建是 Rollup 打包 + esbuild 压缩 的组合策略,兼顾了产物质量和构建速度。

此外,Vite 团队正在开发 Rolldown——一个用 Rust 编写的 Rollup 兼容打包器。未来 Vite 计划用 Rolldown 统一开发和生产的构建工具,同时获得极致性能和完善的打包能力。

Q4: Vite 的预构建(Pre-bundling)是什么?为什么需要?

答案

预构建(也叫依赖预构建、Pre-bundling)是 Vite 在开发服务器首次启动时,使用 esbuildnode_modules 中的第三方依赖进行预处理的过程。它主要解决两个核心问题:

问题一:CJS 转 ESM——格式兼容性

浏览器只能通过原生 import 加载 ESM 格式的模块,但大量 npm 包仍然只提供 CommonJS 格式(如 reactmoment)。预构建会将这些 CJS 模块转换为 ESM:

预构建前后对比
// ❌ react 只提供 CJS 格式
// node_modules/react/index.js
module.exports = { createElement, useState, useEffect, ... };
// 浏览器无法直接 import CJS 模块!

// ✅ esbuild 预构建后,转为 ESM
// node_modules/.vite/deps/react.js
export { createElement, useState, useEffect, ... };
// 浏览器可以通过原生 import 加载

问题二:依赖合并——减少 HTTP 请求瀑布

有些 ESM 包内部由大量小文件组成,如果浏览器逐个请求这些文件,会造成严重的网络瀑布问题:

请求数量优化
// lodash-es 包含 600+ 个独立的 ESM 文件
import { debounce } from 'lodash-es';

// ❌ 未预构建:浏览器需要串行请求
// debounce.js → _debounce.js → _toNumber.js → _isObject.js → ...
// 可能触发 600+ 个 HTTP 请求(每个文件都需要一次网络往返)

// ✅ 预构建后:esbuild 将整个包合并为单个文件
// node_modules/.vite/deps/lodash-es.js
// 只需要 1 个 HTTP 请求!

预构建的流程

缓存策略——不会每次都重新构建

预构建结果会缓存到 node_modules/.vite 目录,只有以下条件变化时才会重新触发:

  • package.json 中的 dependencies 列表变化
  • 包管理器的 lock 文件(pnpm-lock.yamlpackage-lock.json)变化
  • vite.config.tsoptimizeDeps 相关配置变化
手动控制预构建
import { defineConfig } from 'vite';

export default defineConfig({
optimizeDeps: {
// 强制预构建某些依赖(即使 Vite 没有自动检测到)
include: ['lodash-es', 'axios', 'vue'],
// 排除不需要预构建的依赖(已经是标准 ESM)
exclude: ['my-esm-only-package'],
},
});
面试加分点

可以强制重新预构建:运行 vite --force 或删除 node_modules/.vite 目录。当你更新了某个依赖但预构建缓存没有自动失效时,这个操作很有用。

Q5: Vite 的 HMR 为什么比 Webpack 快?

答案

Vite 的 HMR 之所以比 Webpack 快,根本原因在于架构设计的差异。可以从以下几个维度对比:

1. 更新粒度不同

  • Webpack:文件变更后需要重新构建包含该模块的整个 Chunk,Chunk 越大(包含的模块越多),重建越慢
  • Vite:基于原生 ESM,只需重新编译变更的那一个模块,然后浏览器通过原生 import 重新请求这个模块即可

2. 更新速度与项目规模的关系

性能对比(伪数据示意)
// Webpack HMR:项目越大越慢
// 100 个模块 → HMR 约 200ms
// 1000 个模块 → HMR 约 800ms
// 5000 个模块 → HMR 约 3000ms

// Vite HMR:始终保持毫秒级
// 100 个模块 → HMR 约 50ms
// 1000 个模块 → HMR 约 50ms
// 5000 个模块 → HMR 约 50ms
// Vite 的 HMR 速度与项目总模块数无关!

3. 编译工具不同

维度Webpack HMRVite HMR
编译工具Babel/ts-loader(JS 编写)esbuild(Go 编写)
单文件编译速度数十毫秒数毫秒
TypeScript 处理需要完整的类型检查仅做转译(transpile only)

4. 模块传输方式不同

  • Webpack:需要生成 [hash].hot-update.js 文件,浏览器通过 JSONP/fetch 加载
  • Vite:浏览器直接通过原生 ESM import() 请求变更模块的新 URL(带时间戳参数避免缓存),利用浏览器原生的模块加载机制
Vite HMR 模块加载
// Vite HMR Runtime 收到更新通知后
// 直接用动态 import 加载变更模块(带时间戳避免缓存)
const newModule = await import(`/src/components/Header.vue?t=${Date.now()}`);
// 浏览器原生 ESM 加载 → 无额外序列化/反序列化开销

5. HTTP 协商缓存的利用

Vite 对源码模块的请求使用 304 Not Modified 协商缓存,对预构建的依赖使用 Cache-Control: max-age=31536000,immutable 强缓存。这意味着未变更的模块不需要重新从服务端获取,进一步提升了 HMR 后页面的响应速度。

总结

Vite HMR 快的核心原因:原生 ESM 实现模块级精确替换 + esbuild 毫秒级编译 + 更新量与项目规模解耦。无论项目有多大,HMR 只需要处理变更的那一个模块。

Q6: Vite 在生产环境为什么用 Rollup 而不是 esbuild?

答案

虽然 esbuild 的编译和压缩速度极快(Go 编写,比 JS 工具快 10-100 倍),但 Vite 在生产构建时选择 Rollup 作为打包器,核心原因是 esbuild 在产物优化方面的能力尚不成熟

具体原因对比

维度esbuildRollup
代码分割基础支持,策略不灵活成熟的自动/手动分割,支持 manualChunks
CSS 代码分割支持不完善完善支持,CSS 与 JS chunk 精确对应
Tree Shaking基础支持深度 Tree Shaking,Scope Hoisting
插件生态生态较新,插件有限庞大成熟的插件生态
产物格式ESM / CJS 为主支持 ESM、CJS、UMD、IIFE、SystemJS
产物体积较大更小(更彻底的死代码消除)
稳定性快速迭代中,API 可能变化经多年生产验证,高度稳定

代码分割能力对比

Rollup 的灵活代码分割
import { defineConfig } from 'vite';

export default defineConfig({
build: {
rollupOptions: {
output: {
// Rollup 支持精细化的手动分割策略
manualChunks(id: string): string | undefined {
// 框架核心
if (id.includes('vue') || id.includes('react')) {
return 'framework';
}
// UI 组件库
if (id.includes('ant-design') || id.includes('element-plus')) {
return 'ui-lib';
}
// 工具库
if (id.includes('lodash') || id.includes('dayjs')) {
return 'vendor-utils';
}
// 其他 node_modules
if (id.includes('node_modules')) {
return 'vendor';
}
},
},
},
},
});

esbuild 目前无法实现如此灵活的分割策略,这对生产环境的加载性能至关重要。

CSS 代码分割的重要性

生产环境需要将 CSS 按异步 chunk 拆分,确保每个路由只加载所需的 CSS:

CSS 代码分割示意
// 路由 /home → 加载 home.[hash].js + home.[hash].css
// 路由 /about → 加载 about.[hash].js + about.[hash].css
// Rollup 能精确实现 CSS 与 JS chunk 的一一对应
// esbuild 在这方面支持不完善

esbuild 在生产构建中的角色

esbuild 并非完全不参与生产构建

esbuild 在 Vite 生产构建中承担 代码压缩(minify) 的工作,替代传统的 Terser

vite.config.ts
export default defineConfig({
build: {
minify: 'esbuild', // 默认值,比 terser 快 20-40 倍
// minify: 'terser', // 也可以切换为 terser(需安装)
},
});

所以 Vite 的生产构建实际是 Rollup 打包 + esbuild 压缩 的组合策略,兼顾产物质量和构建速度。

未来展望——Rolldown

Vite 团队正在开发 Rolldown——一个用 Rust 编写的、与 Rollup API 兼容的打包器。它的目标是:

  • 兼容 Rollup 的插件生态和配置
  • 拥有接近 esbuild 的构建速度
  • 统一 Vite 的开发和生产构建工具

这意味着未来 Vite 有望用 Rolldown 同时解决开发速度和生产产物质量的问题。

相关链接