面试题 1:Webpack 和 Vite 的核心差异是什么?
核心思路(一句话)
Webpack 开发模式以"构建模块依赖图 + Bundle"为核心;Vite 开发模式以"原生 ESM 按需加载 + 依赖预构建"为核心。
text
Webpack Dev
源码
↓
解析依赖
↓
构建 Module Graph
↓
Loader
↓
Plugin
↓
Bundle
↓
浏览器
text
Vite Dev
源码
↓
浏览器请求入口
↓
原生 ESM
↓
Vite 按需转换模块
↓
浏览器继续请求依赖
所以真正的区别是:
开发阶段两者采用了不同的模块交付模型。
Vite 生产构建仍然需要构建和优化产物。
面试题 2:Vite 的依赖预构建到底做了什么?
这是最重要的追问之一。
核心思路(一句话)
Vite 会在开发启动阶段识别 node_modules 中的依赖,对需要处理的依赖进行预构建,主要解决 CommonJS/UMD 兼容和大量模块请求问题,并缓存结果。
流程
text
node_modules
↓
扫描依赖
↓
识别需要预构建的依赖
↓
esbuild
↓
CommonJS / UMD 等
↓
转换 / 打包成浏览器可消费的 ESM
↓
缓存
↓
浏览器直接请求预构建结果
例如项目:
js
import lodash from 'lodash';
如果 lodash 的发布形式不适合浏览器直接作为 ESM 消费,Vite 会通过依赖预构建处理。
为什么一定要预构建?
主要有两个原因。
原因 1:CommonJS 兼容
例如:
js
const foo = require('./foo');
module.exports = foo;
浏览器原生 ESM 不认识 CommonJS。因此需要:
text
CommonJS
↓
预构建
↓
ESM
↓
浏览器
原因 2:减少大量模块请求
假设某个依赖内部:
text
library
├── a.js
├── b.js
├── c.js
├── d.js
├── e.js
└── ...
如果全部按照原始模块交给浏览器,可能产生大量请求。
预构建可以把依赖处理成更适合开发环境消费的形式:
text
很多内部模块
↓
依赖预构建
↓
优化后的依赖产物
↓
浏览器
Vite 会对依赖进行预构建和优化,具体输出形式由依赖结构和配置决定,不应该简单理解为所有依赖都被无条件打成一个文件。
面试题 3:Vite 是怎么把 CommonJS 转成 ESM 的?
核心思路
不是浏览器自己转换,而是 Vite 在开发服务器阶段借助依赖预构建工具链提前处理。
例如:
js
// CommonJS
const foo = require('./foo');
module.exports = {
foo
};
经过转换后,需要形成浏览器能够消费的 ESM 形式。可以抽象理解成:
text
CommonJS
↓
静态分析
↓
识别 require / exports / module.exports
↓
转换为 ESM 可消费形式
↓
缓存预构建结果
但是有一个边界
CommonJS 本身允许:
js
const name = getName();
require(name);
这种动态依赖。这就无法像标准 ESM 那样天然进行完整静态分析。所以:
CommonJS → ESM 并不是简单的字符串替换,而是构建工具对 CommonJS 模块语义进行分析和兼容处理。
面试题 4:Webpack HMR 和 Vite HMR 底层到底有什么区别?
这是非常好的 30K+ 追问。
核心思路(一句话)
Webpack HMR 的核心是"重新编译受影响模块并通过 HMR Runtime 替换";Vite HMR 的核心是"开发服务器只重新处理发生变化的模块,并通过 ESM 模块边界传播更新"。
Webpack HMR
text
修改 foo.js
↓
Webpack 监听文件变化
↓
重新编译受影响模块
↓
生成 HMR 更新信息
↓
Dev Server 通知浏览器
↓
浏览器请求更新内容
↓
Webpack Runtime
↓
执行模块更新
↓
HMR Accept / Dispose
↓
页面局部更新
所以不能简单说:
Webpack 改一个文件就"全量重新构建"。
实际是:
Webpack HMR 会尽可能只重新编译受影响的模块和相关依赖,但它仍然依赖 Webpack 的模块图和编译流水线。
Vite HMR
text
修改 foo.js
↓
Vite Dev Server
↓
定位发生变化的模块
↓
重新转换 foo.js
↓
WebSocket 通知浏览器
↓
浏览器重新请求 foo.js
↓
ESM 模块重新执行 / HMR 边界处理
↓
页面更新
text
文件变化
↓
Vite Module Graph
↓
找到受影响模块
↓
失效缓存
↓
WebSocket 通知客户端
↓
客户端 HMR Runtime
↓
沿依赖关系传播
↓
执行对应模块更新
面试题 5:为什么 Vite HMR 通常更快?
核心矛盾
Webpack:
text
文件变化
↓
进入 Webpack 编译体系
↓
重新处理受影响模块
↓
Loader / Plugin
↓
生成更新结果
↓
浏览器
Vite:
text
文件变化
↓
定位模块
↓
重新转换这个模块
↓
浏览器重新请求
因此项目越大:
text
Webpack
依赖图 + 构建流程
↓
变化模块处理成本
Vite
开发环境
↓
变化模块按需处理
大型项目中两者的开发体验差异会被放大。
面试题 6:Vite 按需加载几百个模块,会不会产生请求瀑布?
核心思路
会增加浏览器模块请求数量,但"几百个请求 = 必然严重瀑布"是不准确的。
浏览器加载:
text
index.html
↓
main.js
↓
A.js
B.js
C.js
↓
A1.js
A2.js
B1.js
...
如果模块之间存在深层依赖:
text
A
↓
B
↓
C
↓
D
确实可能形成请求链。
但现代浏览器:
- HTTP/2 多路复用
- HTTP/3
- 浏览器缓存
- 并发请求
- Vite 依赖预构建
都会降低大量模块请求的实际成本。
所以不能说:
Vite 靠 HTTP/2 解决了请求瀑布。
更准确:
HTTP/2/HTTP/3 可以降低大量模块请求的连接与队头阻塞成本,但无法从根本上消除存在依赖关系时的请求链。
面试题 7:为什么 Vite 开发环境可以接受很多模块请求?
因为它优化的是:
开发阶段的反馈速度,而不是生产环境最终网络交付效率。
这是 Vite 很重要的架构取舍。
text
Vite
│
┌─────────┴─────────┐
↓ ↓
开发环境 生产环境
↓ ↓
原生 ESM 按需加载 构建优化
↓ ↓
快速启动/HMR Bundle / Chunk
↓
Tree Shaking
↓
Code Splitting
↓
浏览器生产加载
因此:
Vite 开发环境和生产环境本来就不是完全相同的模块交付方式。
面试题 8:为什么 Vite 生产环境还需要打包?
因为生产环境目标变了。
开发环境:
text
目标:
快速启动
快速 HMR
快速反馈
生产环境:
text
目标:
减少请求
优化代码
Tree Shaking
Code Splitting
压缩
缓存
资源优化
因此生产构建需要:
text
源码
↓
Module Graph
↓
静态分析
↓
Tree Shaking
↓
Code Splitting
↓
Chunk
↓
Minify
↓
Production Assets
Vite 的生产构建长期以来基于 Rollup;现代 Vite 版本的底层实现持续演进,因此面试时不要死记成:
"Vite = Rollup"。
更准确:
Vite 是开发服务器 + 构建工具体系,生产构建使用 Rollup 生态进行构建优化,并且其底层实现会随版本演进。
面试题 9:Vite 为什么要采用"开发和生产两套模型"?
核心思路
用开发环境的按需 ESM 换取开发速度,再用生产构建解决最终交付性能。
text
开发阶段
原生 ESM
↓
启动快
↓
HMR 快
↓
开发体验好
生产阶段
完整构建
↓
Tree Shaking
↓
Code Splitting
↓
Chunk 优化
↓
压缩
↓
缓存
这是一种典型的:
开发体验和生产交付效率分别优化。
面试题 10:Webpack 为什么没有采用 Vite 这种开发模式?
这题不要回答成:
"Webpack 老了。"
真正原因是架构模型不同。
Webpack 从设计之初就是:
text
模块
↓
Module Graph
↓
统一编译
↓
Bundle
大量能力建立在这个统一构建模型之上:
text
Loader
Plugin
Chunk
Runtime
Module Graph
Tree Shaking
Code Splitting
HMR
而 Vite 开发模式更加依赖:
text
浏览器原生 ESM
+
开发服务器按需转换
+
依赖预构建
+
Module Graph
所以两者是:
text
Webpack
"先构建,再交付"
Vite Dev
"浏览器请求什么,我处理什么"
面试题 11:如果项目有几千个模块,Vite 开发环境是不是一定比 Webpack 好?
不能绝对化。
应该分析:
text
项目规模
+
模块依赖深度
+
CommonJS 数量
+
插件复杂度
+
浏览器环境
+
HMR 场景
+
开发机性能
特别是:
text
大量深层 ESM 依赖
↓
浏览器请求链增加
↓
可能出现模块请求开销
而 Webpack:
text
开发阶段提前构建
↓
浏览器拿到 Bundle / Chunk
↓
请求数量较少
因此两者实际上是:
| Webpack Dev | Vite Dev | |
|---|---|---|
| 核心模式 | Bundle | Native ESM |
| 启动 | 构建后启动 | 按需提供 |
| HMR | 编译 + HMR Runtime | 模块失效 + ESM HMR |
| CommonJS | Loader/构建体系处理 | 依赖预构建 |
| 浏览器请求 | 相对少 | 开发阶段可能更多 |
| 生产 | Bundle | Production Build |
最重要的底层架构图
把这道题最终压缩成这一张图:
text
Webpack vs Vite
│
┌──────────────┴──────────────┐
↓ ↓
Webpack Vite
│ │
Bundle-first ESM-first
│ │
↓ ↓
Module Graph Browser ESM
│ │
Loader + Plugin 按需转换模块
│ │
↓ ↓
Bundle / Chunk Dependency Pre-bundle
│ │
↓ ↓
Browser Browser
│
↓
HMR
最后:面试官真正想听什么?
如果面试官问:
"Webpack 和 Vite 核心差异是什么?"
不要只说:
Vite 快,因为原生 ESM。
直接这样回答:
Webpack 和 Vite 最大的区别不是性能参数,而是开发阶段的构建模型。Webpack 以构建 Module Graph 和 Bundle 为核心,文件变化后需要经过构建体系重新处理受影响模块,再通过 HMR Runtime 更新;Vite 开发环境则利用浏览器原生 ESM,源码模块按需转换和交付,同时通过依赖预构建解决 CommonJS 和大型依赖的开发性能问题。
所以 Vite 的优势来自"把开发阶段的 Bundle 工作尽量推迟或减少",而不是简单地"不打包"。
但这也带来一个架构取舍:开发阶段浏览器可能面对更多模块请求,因此 Vite 在生产环境仍然需要完整构建,通过 Tree Shaking、Code Splitting、Chunk 优化和压缩等手段生成适合生产交付的资源。
如果继续往下追,我会重点从三个方向展开:依赖预构建如何处理 CommonJS、HMR 如何通过 Module Graph 精确定位更新,以及开发阶段原生 ESM 和生产 Bundle 之间为什么必须存在这种差异。
这道题真正要背的只有 6 句话
text
1. Webpack:Bundle-first。
2. Vite Dev:Native ESM-first。
3. Vite 不是"不打包",而是开发阶段尽量避免全量 Bundle。
4. 依赖预构建:主要解决依赖兼容、请求数量和缓存问题。
5. HMR:Webpack 依赖构建体系 + HMR Runtime;Vite 依赖 Dev Server + Module Graph + ESM HMR。
6. Vite:开发追求反馈速度,生产再通过完整构建追求最终交付性能。