Skip to content

第 17 章 · 面试题集(构建工具)

Q1:Vite 为什么比 Webpack 启动快?

考察点:构建工具底层差异

标准答案

Vite 启动快有三大原因:

  1. 不打包:开发模式利用浏览器原生 ESM,按需编译,请求哪个文件才编哪个
  2. 依赖预构建用 esbuild:把 node_modules 里的 CJS/UMD 一次性转成 ESM 缓存(esbuild 用 Go 写、多线程,比 Webpack 的 JS 解析快 10~100 倍)
  3. 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)
modedevelopment / 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)。"摇树"——没用的叶子被摇掉。

满足条件

  1. 使用 ES Modulesimport/export)—— 静态结构才能可靠分析
  2. 不能有"副作用"(修改全局、注册插件、CSS import 等)
  3. 库需要在 package.json 标注 "sideEffects": false 或列出有副作用的文件
  4. 引用方式要对:import { x } from 'lib' 而非 import lib from 'lib'
  5. 构建器开启 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,按需加载,减少首屏体积。

三种实现方式

  1. 多入口entry: { home: ..., admin: ... }
  2. 动态 importimport('./Page') 自动单独打 chunk
  3. 公共依赖提取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 极快,但作为生产打包器还有几个不足:

  1. 代码分割能力弱:splitChunks 不如 Rollup 智能
  2. CSS 处理简陋:没有像 PostCSS 那样的成熟生态
  3. 插件生态有限:很多生产优化插件只在 Rollup 上有
  4. 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
写自己的工具链/CLIesbuild
需要极致定制(微前端 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 体积太大怎么优化?

考察点:性能优化全面性

标准答案

七大常用手段:

  1. Tree Shaking:移除未使用代码(用 ESM、命名 import)
  2. 代码分割:动态 import + 路由懒加载
  3. 依赖分析与替换:moment → dayjs(小 90%)、lodash → lodash-es 或自己实现
  4. 公共依赖抽取:splitChunks 把 vendor 单独打
  5. 压缩:开启 minify、gzip/brotli 服务端压缩
  6. 图片优化:webp/avif 格式、按 size 转 base64
  7. 按需引入 UI 库:antd 用 babel-plugin-import 或 unplugin-vue-components

分析工具

  • vite-bundle-visualizer
  • webpack-bundle-analyzer
  • rollup-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",怎么定位?