面试题 1:Webpack 迁移到 Vite / Rspack,真正难在哪里?
核心思路(一句话)
迁移不是“换配置”,而是评估模块规范、Loader/Plugin 生态、运行时行为、构建产物和 CI 基建的兼容性。
结构图
Webpack │ ├─ Loader / Plugin ├─ CommonJS / ESM ├─ require.context ├─ 环境变量 ├─ CSS / Less / Sass ├─ 多入口 ├─ Source Map └─ CI / 产物 / 监控 │ ▼ 迁移兼容性评估 │ ┌────┴────┐ ▼ ▼ Vite Rspack │ │ 原生 ESM Webpack 兼容生态 开发模式 Rust 高性能实现一、第一步不是迁移,而是盘点
重点检查:
| 维度 | 需要检查什么 |
|---|---|
| 模块 | CommonJS、ESM、动态require |
| Webpack API | require.context等 |
| Loader | 自定义 Loader、特殊 Loader |
| Plugin | Webpack 专属 Plugin |
| CSS | Less/Sass 全局变量、Loader 配置 |
| 环境变量 | process.env等 |
| 多入口 | MPA、多页面配置 |
| 产物 | 文件名、目录结构、HTML 注入 |
| Source Map | 错误监控上传 |
| CI/CD | 构建命令、缓存、部署流程 |
主要矛盾:构建生态兼容性。
次要矛盾:配置语法差异。
所以:
配置改写通常不是最难的,
真正困难的是 Webpack 生态和项目代码对 Webpack 特有能力的依赖。
面试题 2:Webpack 为什么迁移到 Vite 后,开发环境会明显变快?
核心思路(一句话)
Webpack 开发模式通常需要先构建模块依赖图,而 Vite 开发模式利用浏览器原生 ESM 按需提供模块,并对依赖进行预构建。
传统 Webpack
源码 ↓ 解析依赖 ↓ 构建 Module Graph ↓ Loader ↓ Plugin ↓ 生成 Bundle ↓ 浏览器项目越大,初始构建涉及的模块越多。
Vite 开发模式
浏览器请求页面 ↓ 原生 ESM ↓ 按需请求模块 ↓ Vite 转换当前模块 ↓ 浏览器执行同时:
node_modules ↓ 依赖预构建 ↓ CommonJS → ESM 等处理 ↓ 缓存因此 Vite 的核心优势不是简单的“代码比 Webpack 快”,而是:
开发阶段减少了传统 Bundle 全量构建的成本。
面试题 3:Webpack → Vite 迁移,常见坑有哪些?
1. 环境变量
Webpack 项目常见:
process.env.NODE_ENVprocess.env.API_URLVite 常见:
import.meta.env.MODEimport.meta.env.VITE_API_URL例如:
if(import.meta.env.DEV){console.log('development');}注意:
不能简单认为 Vite 会自动把所有process.env替换掉。
尤其是第三方依赖中的:
process.env.NODE_ENV需要结合依赖兼容策略处理。
2. CommonJS 依赖
老项目中经常存在:
constlodash=require('lodash');或者依赖包本身就是 CommonJS。Vite 开发环境通常会通过依赖预构建处理 CommonJS 依赖,但不能理解成:
“Vite 只支持 ESM。”
更准确地说:
项目源码 │ ├── ESM │ ↓ │ 原生处理 │ └── CommonJS 依赖 ↓ 依赖预构建 ↓ ESM因此迁移时重点检查:
- CommonJS 依赖
- 动态
require - 非标准模块行为
- Node.js 内置模块依赖
面试题 4:require.context为什么是 Webpack → Vite 迁移的大坑?
核心思路(一句话)
require.context是 Webpack 提供的编译时模块上下文 API,而 Vite 没有直接等价 API。
Webpack:
constmodules=require.context('./components',true,/\.js$/);modules.keys().forEach(key=>{constmodule=modules(key);});Webpack 会在构建阶段分析:
./components │ ├── A.js ├── B.js ├── C.js └── D.js然后构造一个模块上下文。
Vite 的对应方案
通常使用:
import.meta.glob('./components/*.js')例如:
constmodules=import.meta.glob('./components/*.js');得到的是:
{'./components/A.js':()=>import('./components/A.js'),'./components/B.js':()=>import('./components/B.js'),'./components/C.js':()=>import('./components/C.js')}如果希望直接加载:
constmodules=import.meta.glob('./components/*.js',{eager:true});关键区别
Webpack require.context() ↓ Webpack 专属 API ↓ 构建阶段生成模块上下文 Vite import.meta.glob() ↓ Vite 专属编译能力 ↓ 构建阶段展开 glob所以不能说:
“Vite 没有 require.context,所以全部手动改 import。”
更准确的回答是:
需要识别require.context的实际业务语义,再使用import.meta.glob、显式 import 或其他方案重构。
面试题 5:如果项目里有 200 个require.context,怎么办?
这是非常好的架构师追问。
错误答案
全部改成
import.meta.glob。
因为这只是机械替换,没有解决迁移成本问题。
正确思路
200 个 require.context ↓ 统计使用场景 ↓ 分类 ┌──────┼────────┐ ▼ ▼ ▼ 组件扫描 路由扫描 自动注册 └──────┼────────┘ ↓ 设计统一迁移方案 ↓ 批量 Codemod / AST 转换 ↓ 测试行为一致性例如可以通过AST/Codemod自动识别:
require.context('./components',true,/\.vue$/)再生成:
import.meta.glob('./components/**/*.vue')真正体现工程化能力的是:
不是“我知道怎么改”,而是“我知道怎么低成本批量迁移”。
面试题 6:Rspack 为什么更容易从 Webpack 迁移?
核心思路
Rspack 的核心目标之一就是保持较高的 Webpack 生态兼容性,同时使用 Rust 实现核心构建能力。
架构可以理解成:
Webpack 项目 │ ├── webpack.config.js ├── Loader ├── Plugin └── Webpack API │ ▼ Rspack │ Rust 核心实现 │ ▼ 更快的构建性能因此对于大型 Webpack 项目:
Webpack → Rspack通常比:
Webpack → Vite更容易保持原有构建模型。
但这里一定要注意:
“兼容 Webpack” ≠ “100% 所有 Loader/Plugin 都兼容”。
迁移前仍然需要验证:
- Loader
- Plugin
- Webpack API
- 自定义构建逻辑
- Module Federation
- CSS
- HTML
- Source Map
- CI/CD
面试题 7:Webpack → Rspack 最大的迁移优势是什么?
一句话
不是因为 Rspack 配置长得像 Webpack,而是因为它尽量保持 Webpack 的模块、Loader、Plugin 和配置模型,从而降低迁移成本。
尤其是大型项目:
Webpack │ ├── 复杂 Loader ├── Plugin ├── 自定义构建逻辑 └── Webpack 生态 │ ▼ Rspack │ 尽量复用现有体系这和 Vite 的思路不同:
Webpack → Vite更多是:
重新适配构建模型。
而:
Webpack → Rspack更接近:
保留构建模型,替换底层实现。
面试题 8:CommonJS 和 ESM 的 Tree Shaking 本质区别是什么?
这是整段内容里最值得掌握的核心题。
核心思路(一句话)
Tree Shaking 的关键不是“ESM 天生能摇树”,而是 ESM 的导入导出关系具有静态结构,构建工具可以在编译阶段确定模块依赖和未使用导出。
ESM
// utils.jsexportfunctionadd(){}exportfunctionsub(){}exportfunctionmultiply(){}import{add}from'./utils.js';add();构建工具可以分析:
utils.js ├── add ← 使用 ├── sub ← 未使用 └── multiply ← 未使用于是:
保留 add 删除 sub 删除 multiplyCommonJS
constutils=require('./utils');utils.add();甚至可以:
constmoduleName=getModuleName();constmodule=require(moduleName);依赖路径可能直到运行时才能确定。
因此:
ESM 静态 import/export ↓ 编译阶段分析 ↓ 确定依赖关系 ↓ Tree Shaking CommonJS 动态 require ↓ 依赖关系可能运行时确定 ↓ 静态分析困难 ↓ Tree Shaking 受限面试题 9:是不是 CommonJS 就完全不能 Tree Shaking?
CommonJS 的动态特性使可靠、完整的静态 Tree Shaking 很困难,但现代构建工具可以对部分 CommonJS 代码进行静态分析或转换,从而获得一定程度的优化。
因此面试不要回答:
CommonJS 不能 Tree Shaking。
应该回答:
ESM 的静态模块结构天然适合 Tree Shaking;CommonJS 由于
require()的动态性,静态分析能力受限。构建工具可以通过 CommonJS → ESM 转换和静态分析处理部分场景,但优化能力和可靠性通常不如原生 ESM。
这才是准确的高阶答案。
面试题 10:Tree Shaking 真正依赖哪些条件?
① 模块必须具有可静态分析的结构
优先:
import{foo}from'./utils.js';而不是:
require(variable);② 构建工具需要正确识别副作用
例如:
{"sideEffects":false}它表示:
模块可以被认为没有副作用,因此未使用的模块代码可以安全删除。
但要注意:
sideEffects: false不是 Tree Shaking 的必要条件。
它主要帮助构建工具进行模块级副作用判断。
③ 代码本身必须允许删除
例如:
exportfunctionfoo(){}exportfunctionbar(){console.log('bar');}如果:
import{foo}from'./utils.js';那么bar可能被删除。
但如果模块存在:
import'./register.js';而register.js执行:
window.xxx=...它就存在副作用,不能简单删除。
面试题 11:sideEffects: false和 Tree Shaking 是什么关系?
这是很容易被问深的问题。
{"sideEffects":false}本质是在告诉构建工具:
这个包中的模块 如果没有被使用 可以认为不存在必须保留的顶层副作用例如:
// polyfill.jsArray.prototype.foo=function(){};这种代码明显具有副作用。如果错误配置:
{"sideEffects":false}可能导致构建工具把:
import'./polyfill.js';错误地认为可以删除。
所以:
sideEffects: false配错可能导致运行时功能丢失。
面试题 12:为什么 CommonJS 依赖会影响 Tree Shaking?
假设:
// CommonJSmodule.exports={foo,bar,baz};应用:
const{foo}=require('./utils');构建工具很难像 ESM 一样直接利用:
exportfooexportbarexportbaz这样的静态导出关系。而 ESM:
export{foo};export{bar};export{baz};模块导出关系在语法层面就是明确的。所以:
ESM ↓ 静态 Module Graph ↓ Exports Analysis ↓ Used Exports Analysis ↓ Dead Code Elimination ↓ Tree Shaking这是面试时最值得讲的完整链路。
面试题 13:Barrel Export 为什么可能影响 Tree Shaking?
例如:
// index.jsexport*from'./a';export*from'./b';export*from'./c';export*from'./d';业务代码:
import{foo}from'./index.js';现代构建工具通常仍然能够进行 Tree Shaking,不能简单说 Barrel Export 一定会导致 Tree Shaking 失效。
但 Barrel Export 可能:
- 增加模块图复杂度
- 增加分析成本
- 让依赖关系更加间接
- 在某些工具链、CommonJS 混用等情况下削弱优化效果
所以面试最好说:
Barrel Export 不是 Tree Shaking 失效的必然原因,但过度使用会增加模块图复杂度,在复杂依赖和 CommonJS 混用场景下可能影响优化效果。
最后一个架构师级问题:Webpack → Vite / Rspack 到底怎么做?
可以直接背这套:
① 现状评估 ↓ ② 统计 Webpack 特性使用情况 ↓ ③ 检查 CommonJS / ESM ↓ ④ 检查 Loader / Plugin ↓ ⑤ 检查 require.context 等 Webpack API ↓ ⑥ 检查环境变量 / CSS / 多入口 ↓ ⑦ 检查 CI / Source Map / 产物 ↓ ⑧ 建立兼容性清单 ↓ ⑨ 小范围 POC ↓ ⑩ 性能 + 产物 + 行为对比 ↓ ⑪ 批量迁移 / Codemod ↓ ⑫ 灰度验证 ↓ ⑬ 切换构建链面试“满分答案”
Webpack 迁移到 Vite 或 Rspack,我不会先改配置,而是先做兼容性和收益评估。
第一,看项目是否依赖 Webpack 特有能力,比如
require.context、自定义 Loader、Plugin,以及大量 CommonJS 依赖。第二,看目标工具的构建模型。Vite 开发环境核心是浏览器原生 ESM + 依赖预构建,因此开发体验和 Webpack 的 Bundle 模型存在明显差异;Rspack 则更强调 Webpack 生态兼容,通常更适合大型存量 Webpack 项目渐进迁移。
第三,重点验证环境变量、CommonJS、动态
require、CSS、Source Map、多入口、CI/CD 和构建产物。**其中最容易被低估的是 CommonJS 和 Webpack 特有 API。**例如
require.context没有直接等价物,需要根据业务场景改造成import.meta.glob或其他模块发现方案;如果有大量调用,还应该考虑通过 AST/Codemod 批量迁移,而不是人工修改。Tree Shaking 方面,核心不是简单记忆“ESM 能摇、CommonJS 不能摇”,而是理解背后的原因:ESM 的
import/export具有静态结构,构建工具可以在编译阶段建立 Module Graph、分析 Used Exports,再进行 Dead Code Elimination;CommonJS 的require()可以动态执行,依赖关系可能运行时才能确定,因此静态分析和 Tree Shaking 能力受到限制。最后我会通过 POC 对比构建速度、开发启动、HMR、产物体积、运行时行为、Source Map、内存和 CI 稳定性,确认迁移收益覆盖迁移成本后,再逐步切换。
所以,构建工具迁移的本质不是“把 webpack.config.js 改成另一种配置”,而是一次构建体系迁移。
真正需要记住的 5 个点
1. Vite ≠ Webpack 换皮 → 开发构建模型不同 2. Rspack → 尽量兼容 Webpack 生态 → 降低存量项目迁移成本 3. require.context → Webpack 特有能力 → Vite 没有直接等价 API → import.meta.glob 是常见替代方案 4. ESM Tree Shaking → 核心是“静态可分析” → Module Graph → Used Exports → Dead Code Elimination 5. CommonJS → 不是“绝对不能 Tree Shaking” → 而是动态 require 限制静态分析 → 转 ESM 后可能获得更多优化机会这 5 个点串起来,基本就从“会配置 Webpack”提升到了真正理解构建工程化迁移的层次。