主题
第 17 章 · 面试题集(构建工具)
Q1:Vite 为什么比 Webpack 启动快?
考察点:构建工具底层差异
标准答案:
Vite 启动快有三大原因:
- 不打包:开发模式利用浏览器原生 ESM,按需编译,请求哪个文件才编哪个
- 依赖预构建用 esbuild:把 node_modules 里的 CJS/UMD 一次性转成 ESM 缓存(esbuild 用 Go 写、多线程,比 Webpack 的 JS 解析快 10~100 倍)
- HMR 颗粒度更细:只编译变更的那个文件,不重新打包整个 chunk
Webpack 必须先把所有文件依赖图建好、打包成 bundle 才能启动 dev server,项目越大启动越慢(线性恶化)。
通俗理解:
Webpack 像"翻译完整本书才让你看",Vite 像"你翻到第几页才翻译第几页"。
代码示例:
bash
# 中型项目(约 1000 个模块)
$ time webpack-dev-server
✓ ready in 18.3s
$ time vite
✓ ready in 286ms加分回答:
- Vite 生产模式仍用 Rollup 打包,因为 esbuild 的代码分割和 CSS 处理还不够成熟
- 浏览器原生 ESM 在请求大量小文件时,HTTP/2 多路复用让"小文件多"不再是性能问题
- Vite 的依赖预构建结果会缓存到
node_modules/.vite,二次启动更快 - 当模块数量极大(几万)时,Vite 浏览器侧的请求数会暴增,此时 Turbopack 这类"按需打包"方案更优
追问:
- 为什么 Vite 不在生产模式也用 esbuild 直接打包?
- HTTP/2 普及前,Vite 这种"不打包"方案能用吗?
Q2:Webpack 五大核心概念是什么?
考察点:Webpack 基础
标准答案:
| 概念 | 作用 |
|---|---|
| entry | 入口文件,从这里开始构建依赖图 |
| output | 输出位置、文件名、公共路径 |
| loader | 让 Webpack 能处理非 JS 文件(如 css-loader、ts-loader) |
| plugin | 介入构建生命周期做事(HtmlWebpackPlugin、DefinePlugin) |
| mode | development / production / none,决定内置优化 |
loader vs plugin 区别:
- loader 是文件转换器,按
test正则匹配文件后转换内容 - plugin 是构建生命周期钩子,能改打包流程的方方面面
通俗理解:
把 Webpack 比作汽车工厂:
- entry = 原料从哪进
- loader = 流水线工人,把零件加工成标准件
- plugin = 厂长,决定生产计划、加装备份机器、统计报表
- output = 成品车从哪出
- mode = 今天是练手日还是出货日
代码示例:
js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
path: __dirname + '/dist',
clean: true
},
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader' },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
{ test: /\.(png|jpg)$/, type: 'asset/resource' }
]
},
plugins: [new HtmlWebpackPlugin({ template: './index.html' })],
mode: 'production'
};加分回答:
- loader 执行顺序:从右到左、从下到上(链式调用)
- plugin 通过 tapable 钩子机制工作(compiler.hooks.xxx.tap)
- mode: production 自动开启 TerserPlugin(压缩)、Tree Shaking、Scope Hoisting
- 不写 mode 会有警告,且默认 production
- Webpack 5 的 module federation 支持微前端
追问:
- ts-loader 和 babel-loader 处理 TS 有什么区别?
- 怎么排查"哪个 loader/plugin 让构建变慢了"?
Q3:什么是 Tree Shaking?需要满足什么条件?
考察点:构建优化
标准答案:
Tree Shaking = 在打包时移除未使用的代码(dead code elimination)。"摇树"——没用的叶子被摇掉。
满足条件:
- 使用 ES Modules(
import/export)—— 静态结构才能可靠分析 - 不能有"副作用"(修改全局、注册插件、CSS import 等)
- 库需要在
package.json标注"sideEffects": false或列出有副作用的文件 - 引用方式要对:
import { x } from 'lib'而非import lib from 'lib' - 构建器开启 production 模式(一般同时开启了压缩,才能真正移除代码)
通俗理解:
ESM 像"图书馆借书登记表"——能确定哪本书没人借就撤下书架。CJS 像"读者随便拿书也不登记"——管理员不敢撤任何书。
代码示例:
js
// utils.js
export function used() { return 1; }
export function unused() { return 2; } // 没人用
// main.js
import { used } from './utils';
used();
// 打包后只剩 used,unused 被摇掉
// ❌ 失效场景:
import * as utils from './utils'; // 全部导入,无法摇
import _ from 'lodash'; // 默认导入老 lodash(CJS),无法摇加分回答:
- Webpack 的 Tree Shaking 分两步:标记未使用 + Terser 删除
- Rollup 的 Tree Shaking 更激进、产物更干净(库打包首选)
- 副作用的典型例子:
import 'core-js/stable'—— 只为副作用而 import,没导出任何东西 - 有些库(如 antd)默认导入会全量打包,需要配合
babel-plugin-import做按需引入 - 检查是否生效:用
webpack-bundle-analyzer看 bundle 内容
追问:
sideEffects: false写错了会导致什么问题?- 怎么排查 lodash 没被 Tree Shaking?
Q4:什么是代码分割(Code Splitting)?怎么实现?
考察点:性能优化与打包策略
标准答案:
Code Splitting = 把一个大 bundle 拆成多个小 chunk,按需加载,减少首屏体积。
三种实现方式:
- 多入口:
entry: { home: ..., admin: ... } - 动态 import:
import('./Page')自动单独打 chunk - 公共依赖提取:
splitChunks配置
最常用的是动态 import + 路由懒加载。
通俗理解:
不要把 1000 道菜端到一张桌上压垮地板,按"前菜桌、主餐桌、甜品桌"分开,客人走到哪桌再上哪桌的菜(懒加载)。
代码示例:
jsx
// React 路由懒加载
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
<Suspense fallback={<Loading />}>
<Dashboard />
</Suspense>
// 按需加载重型库
button.onclick = async () => {
const { Chart } = await import('chart.js');
new Chart(...);
};
// Webpack splitChunks 配置
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
}
}
}
}加分回答:
- 动态 import() 是 ECMAScript 标准语法,返回 Promise
- Webpack 的 magic comment:
import(/* webpackChunkName: "chart" */ 'chart') - Vite 默认按
import()自动切分,无需配置 - 配合
rel="prefetch"可在空闲时预加载未来需要的 chunk - 切分粒度过细也有副作用:HTTP 请求数过多、首屏依赖关系复杂
追问:
- 为什么把 vendor(node_modules)单独抽出来?
- HTTP/2 普及后,代码分割策略有变化吗?
Q5:HMR(热模块替换)原理是什么?
考察点:开发体验底层
标准答案:
HMR(Hot Module Replacement)= 修改代码后只替换变更模块,不刷新整个页面、保留应用状态。
原理流程:
1. dev server 与浏览器建立 WebSocket 连接
2. 你保存文件 → dev server 监听到变化(chokidar/fs.watch)
3. dev server 编译变更模块,发送一个补丁消息
4. 浏览器收到消息:调用 module.hot.accept(callback) 执行替换
5. 新模块替换旧模块,引用方按需重新执行框架(React/Vue)会包装这个机制,做到"组件热替换且状态保留"。
通俗理解:
普通刷新像"装修整间屋子",HMR 像"只换了客厅的那盏灯泡"——其他东西都没动。
代码示例:
js
// 原生 HMR API(极少手写,框架插件自动处理)
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
console.log('模块更新了:', newModule);
});
import.meta.hot.dispose(() => {
console.log('旧模块清理');
});
}
// React Fast Refresh(@vitejs/plugin-react 内置)
// Vue HMR(@vitejs/plugin-vue 内置)
// 你只管写代码,热更新自动生效加分回答:
- React Fast Refresh = 官方 HMR 实现,能保留 useState/useReducer 状态
- Vue 单文件组件 HMR = 模板/样式/脚本都能局部热替换
- HMR 失败时的兜底:full reload(整页刷新)
- Vite 的 HMR 比 Webpack 快是因为请求路径明确(不需要重新打包整个 chunk)
- HMR 失效常见原因:组件没正确 export、副作用代码、文件命名包含特殊字符
追问:
- HMR 和 Live Reload 有什么区别?
- HMR 失效时怎么排查?
Q6:Vite 生产模式为什么不直接用 esbuild?
考察点:构建工具组合策略
标准答案:
esbuild 极快,但作为生产打包器还有几个不足:
- 代码分割能力弱:splitChunks 不如 Rollup 智能
- CSS 处理简陋:没有像 PostCSS 那样的成熟生态
- 插件生态有限:很多生产优化插件只在 Rollup 上有
- Tree Shaking 略弱:极致干净的产物 Rollup 更胜一筹
所以 Vite 选择"开发用 esbuild 追极速、生产用 Rollup 追质量"的组合。
通俗理解:
esbuild 像跑车(极快但内饰简单),Rollup 像越野车(稳重、配置全)。日常通勤(开发)用跑车,长途搬家(生产构建)用越野车。
代码示例:
ts
// vite.config.ts
export default defineConfig({
// 开发模式自动用 esbuild
esbuild: { jsxFactory: 'h' },
// 生产模式自动用 Rollup
build: {
rollupOptions: {
output: { manualChunks: { vendor: ['react', 'react-dom'] } }
},
target: 'es2020',
minify: 'esbuild' // 注意:压缩仍可用 esbuild(minify-only),比 terser 快
}
});加分回答:
- Vite 5+ 已实验性支持 Rollup 4 + 完整 ESM 工作流
- esbuild 团队也在补强代码分割,未来可能完全取代 Rollup
- 当前 Vite 默认用 esbuild 做 minify(速度快,效果接近 terser)
- Tauri / Bun 等工具直接用 esbuild 做生产打包,对中小项目够用
追问:
- esbuild 比 terser 压缩效率差多少?
- Rollup 4 相比 3 有什么进步?
Q7:esbuild、Rollup、Webpack、Vite、Turbopack 怎么选?
考察点:构建工具选型
标准答案:
| 场景 | 首选 |
|---|---|
| 写应用(React/Vue/Svelte) | Vite |
| 写 npm 库 | Rollup(或 tsup/unbuild,封装了 Rollup+esbuild) |
| Next.js 项目 | Turbopack(Next.js 默认)/ Webpack |
| 老项目维护 | Webpack |
| 写自己的工具链/CLI | esbuild |
| 需要极致定制(微前端 module federation) | Webpack |
通俗理解:
- Vite = 通勤代步车(日常开发首选)
- Rollup = 长途搬家车(库打包首选)
- Webpack = 重型工程车(功能全但重)
- esbuild = F1 赛车(极快但只跑直道)
- Turbopack = 新能源车(Vercel 全力押注,未来主流)
代码示例:
bash
# 应用
npm create vite@latest my-app
# 库
npm create vue-lib@latest # vue 库
npx create-react-library # react 库(内部用 Rollup)
# 通用 TS 库
npm create tsup-lib # tsup 内部 esbuild
# Next.js(自带 Turbopack)
npx create-next-app加分回答:
- Bun 自带打包器,对小项目可一站式(pkg manager + bundler + runtime)
- Rspack(字节出品)= Rust 重写的 Webpack,兼容 Webpack 配置但快 10 倍
- Farm = 类似 Vite 但生产模式也用 Rust 打包
- 选型时考虑:团队熟悉度 > 项目规模 > 性能极致
追问:
- Rspack 和 Turbopack 的区别?
- 如何把老 Webpack 项目逐步迁移到 Vite?
Q8:bundle 体积太大怎么优化?
考察点:性能优化全面性
标准答案:
七大常用手段:
- Tree Shaking:移除未使用代码(用 ESM、命名 import)
- 代码分割:动态 import + 路由懒加载
- 依赖分析与替换:moment → dayjs(小 90%)、lodash → lodash-es 或自己实现
- 公共依赖抽取:splitChunks 把 vendor 单独打
- 压缩:开启 minify、gzip/brotli 服务端压缩
- 图片优化:webp/avif 格式、按 size 转 base64
- 按需引入 UI 库:antd 用 babel-plugin-import 或 unplugin-vue-components
分析工具:
vite-bundle-visualizerwebpack-bundle-analyzerrollup-plugin-visualizer
通俗理解:
bundle 优化像"打包行李":
- Tree Shaking = 不用的衣服别带
- 代码分割 = 重物分到几个箱子
- 依赖替换 = 大行李箱换成小行李箱
- 压缩 = 真空压缩袋
代码示例:
js
// ❌ 错误:全量引入
import _ from 'lodash';
// ✅ 正确
import debounce from 'lodash/debounce';
// 或更好
import { debounce } from 'lodash-es';
// ❌ 错误:moment.js 体积大
import moment from 'moment';
// ✅ 正确
import dayjs from 'dayjs';
// 配置压缩
build: {
minify: 'terser',
terserOptions: {
compress: { drop_console: true, drop_debugger: true }
}
}加分回答:
- 设置
<link rel="preload">关键 chunk - 用
splitChunks+cacheGroups让长期不变的依赖享受 CDN 缓存 - 开 source-map 但不上线(上传到 Sentry)
- polyfill 按目标浏览器精确生成(@babel/preset-env + browserslist)
- SSG 项目可以做"零 JS"(如 Astro Islands)
- 服务端开 Brotli > Gzip > 不压缩,体积差异可达 20%+
追问:
- 怎么找出"被偷偷打进 bundle 的大依赖"?
- splitChunks 切多细才合适?
Q9(加餐):source map 是什么?生产环境要不要开?
考察点:调试与生产权衡
标准答案:
Source Map = 编译后的代码与源代码之间的映射文件,让浏览器 DevTools / 错误日志能显示原始代码位置。
类型与速度/质量权衡(Webpack devtool):
| 选项 | 速度 | 质量 | 适用 |
|---|---|---|---|
eval-source-map | 快 | 高 | 开发 |
cheap-module-source-map | 中 | 中 | 开发 |
source-map | 慢 | 完整 | 生产 |
hidden-source-map | 慢 | 完整 | 生产(不暴露给用户) |
nosources-source-map | 慢 | 仅行号 | 生产(部分) |
生产环境推荐:生成 source-map 但不部署到 CDN,只上传到 Sentry / Bugsnag 等错误追踪平台。
通俗理解:
source map 像"翻译稿和原稿的对照表"。读者(浏览器)看到压缩后的"翻译稿"乱码,对照表能告诉他原文是哪一句。
加分回答:
- 生产暴露 source map 等于把源码公开(竞争对手能直接看到)
- Sentry 自动按 release version 关联 source map 还原堆栈
- Vite 默认生产模式不生成 source map,需要
build.sourcemap: 'hidden'等 - HTTP header
SourceMap: xxx.map是另一种关联方式
追问:
- source map 文件结构是什么样的?
- 上线后用户报错"app.min.js:1 1 of 1",怎么定位?