跳到主要内容

Webpack 工作原理与工作流程

在前端工程化体系中,Webpack 是最经典、生态最完善的模块打包工具。理解它的内部工作原理,不仅能帮助我们写出更优的构建配置,还能在遇到构建问题时快速定位原因。本文将从 核心概念 出发,逐步深入 Webpack 的 完整工作流程Loader 机制Plugin 机制HMR 热更新 等核心原理。

一、核心概念速览

在深入原理之前,先回顾 Webpack 的几个核心概念,它们是理解整个工作流程的基石:

概念说明类比
Entry(入口)构建的起点,Webpack 从这里开始解析依赖迷宫的入口
Module(模块)一切文件都是模块(JS、CSS、图片等)乐高积木的每一块
Chunk(代码块)一组模块的集合,是打包的中间产物一组积木拼成的部件
Bundle(包)最终输出的文件,由 Chunk 生成组装完成的成品
Loader(加载器)将非 JS 文件转换为 Webpack 能处理的模块翻译官
Plugin(插件)扩展 Webpack 功能,贯穿整个构建生命周期流水线上的工人
Dependency Graph(依赖图)模块之间的引用关系图组织架构图

二、Webpack 完整工作流程总览

Webpack 的构建过程可以分为 三大阶段:初始化、构建(Make)、生成(Seal)。每个阶段都由一系列步骤组成,Plugin 通过 Hook 机制贯穿全程。

下面我们逐一拆解每个阶段。

三、初始化阶段

初始化阶段的核心任务是 合并配置创建 Compiler 对象

3.1 配置合并

Webpack 的配置来源有三个层级:

// 1. 命令行参数(优先级最高)
// webpack --mode production --entry ./src/index.js

// 2. 配置文件 webpack.config.js
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
},
module: { rules: [/* ... */] },
plugins: [/* ... */],
};

// 3. 默认配置(优先级最低)
// Webpack 内置的默认值,如 output.path 默认为 dist

合并顺序:命令行参数 > 配置文件 > 默认配置

3.2 创建 Compiler 对象

Compiler 是 Webpack 的核心引擎,整个构建过程只会创建一个 Compiler 实例。它负责:

  • 保存完整的 Webpack 配置
  • 管理所有 Plugin 的注册与调用
  • 提供文件系统访问能力
  • 触发构建流程的各个 Hook
// 简化版 Compiler 创建过程
const webpack = (options) => {
// 1. 合并配置,填充默认值
options = new WebpackOptionsApply().process(options);

// 2. 创建 Compiler 实例
const compiler = new Compiler(options.context);
compiler.options = options;

// 3. 注册所有插件
if (options.plugins) {
for (const plugin of options.plugins) {
plugin.apply(compiler); // 每个插件在此注册 Hook
}
}

// 4. 触发 environment 和 afterEnvironment 钩子
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

return compiler;
};

3.3 初始化阶段的 Hook 执行顺序

四、构建阶段(Make)

构建阶段是 Webpack 最核心的环节,任务是 从入口出发,递归地构建整个模块依赖图

4.1 整体流程

4.2 创建 Module 对象

每个文件在 Webpack 中都被抽象为一个 Module 对象,其中最常见的是 NormalModule

// NormalModule 的核心结构(简化)
class NormalModule extends Module {
constructor({ type, resource, loaders, parser, generator }) {
this.type = type; // 模块类型(如 'javascript/auto')
this.resource = resource; // 文件绝对路径
this.loaders = loaders; // 匹配到的 Loader 列表
this.parser = parser; // 解析器(如 JavascriptParser)
this.generator = generator; // 代码生成器
this.dependencies = []; // 依赖列表
this._source = null; // Loader 处理后的源码
this._ast = null; // 解析后的 AST
}
}

4.3 Loader 转换

Webpack 只能直接理解 JavaScript 和 JSON,其他类型的文件需要通过 Loader 转换。Loader 的执行分为两个阶段:Pitch 阶段(从左到右)和 Normal 阶段(从右到左)。

Loader 执行顺序的记忆口诀

配置中写在 最后的 Loader 最先执行(Normal 阶段从右到左),就像一个管道:sass-loader → css-loader → style-loader,Sass 文件先被编译成 CSS,再被处理成 JS 模块,最后被注入到 DOM 中。

一个实际的 Loader 示例

// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.css$/,
// 执行顺序:style-loader ← css-loader ← postcss-loader
use: ['style-loader', 'css-loader', 'postcss-loader'],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
},
},
},
],
},
};

4.4 AST 解析与依赖收集

Loader 处理完成后,Webpack 使用内置的 JavascriptParser(基于 acorn)将代码解析为 AST(抽象语法树),然后遍历 AST 节点来收集依赖。

// 原始代码
import React from 'react';
import { Button } from './components/Button';
const lodash = require('lodash');

// Webpack 解析 AST 时会识别以下依赖语句:
// 1. import ... from '...' → ImportDeclaration
// 2. require('...') → CallExpression (callee.name === 'require')
// 3. import('...') → 动态导入,标记为异步依赖
// 4. require.ensure(...) → 旧版异步加载语法

4.5 模块路径解析(Resolve)

收集到依赖后,Webpack 使用 enhanced-resolve 库将模块标识符解析为绝对路径:

// resolve 配置示例
module.exports = {
resolve: {
// 自动补全扩展名,依次尝试
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
// 路径别名
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
},
// 模块搜索目录
modules: ['node_modules', path.resolve(__dirname, 'src')],
// 包入口字段的查找顺序
mainFields: ['browser', 'module', 'main'],
},
};

解析流程

4.6 递归构建依赖图

上述步骤不断重复,直到所有模块及其依赖全部处理完毕,最终形成一棵完整的 依赖图(Dependency Graph)

// 依赖图的概念结构(简化)
const dependencyGraph = {
'src/index.js': {
dependencies: ['src/App.js', 'src/utils.js', 'react'],
source: '/* 转换后的代码 */',
},
'src/App.js': {
dependencies: ['src/components/Header.js', 'react'],
source: '/* 转换后的代码 */',
},
'src/utils.js': {
dependencies: ['lodash'],
source: '/* 转换后的代码 */',
},
// ... 更多模块
};

五、生成阶段(Seal)

构建阶段完成后,Webpack 已经拥有了完整的依赖图。接下来进入 Seal(封装/生成)阶段,核心任务是将模块组装为 Chunk 并生成最终代码。

5.1 Chunk 生成策略

Webpack 根据以下规则决定如何将模块分配到不同的 Chunk:

规则说明示例
Entry Chunk每个入口对应一个 Chunkentry: { app: './src/index.js' }
Async Chunk动态 import() 自动拆分为新 Chunkimport('./page/About')
SplitChunks通过优化配置自动提取公共模块optimization.splitChunks
Runtime ChunkWebpack 运行时代码可单独提取optimization.runtimeChunk

5.2 SplitChunks 优化配置

// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 对所有类型的 Chunk 都进行分割
minSize: 20000, // 最小分割大小(20KB)
minChunks: 1, // 被引用的最少次数
maxAsyncRequests: 30, // 异步加载时的最大并行请求数
maxInitialRequests: 30,// 入口点的最大并行请求数
cacheGroups: {
// 提取第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10,
reuseExistingChunk: true,
},
// 提取公共模块
common: {
minChunks: 2,
name: 'common',
priority: -20,
reuseExistingChunk: true,
},
},
},
// 将 runtime 代码单独提取
runtimeChunk: 'single',
},
};

5.3 代码生成

确定了 Chunk 的划分后,Webpack 为每个 Chunk 生成最终的代码。核心过程如下:

生成的 Bundle 代码结构大致如下:

// 简化版 Webpack 打包输出
(() => {
// 1. 模块集合:所有模块以 moduleId 为 key 存储
var __webpack_modules__ = {
'./src/utils.js': (module, exports, __webpack_require__) => {
// utils.js 的代码
const add = (a, b) => a + b;
exports.add = add;
},
'./src/App.js': (module, exports, __webpack_require__) => {
// App.js 的代码
const { add } = __webpack_require__('./src/utils.js');
console.log(add(1, 2));
},
};

// 2. 模块缓存
var __webpack_module_cache__ = {};

// 3. require 函数实现
function __webpack_require__(moduleId) {
// 检查缓存
if (__webpack_module_cache__[moduleId]) {
return __webpack_module_cache__[moduleId].exports;
}
// 创建新模块并缓存
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// 执行模块代码
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}

// 4. 入口执行
__webpack_require__('./src/App.js');
})();
关键理解

Webpack 的输出本质上是一个 IIFE(立即执行函数),内部实现了一套自己的模块系统:__webpack_modules__ 存模块,__webpack_module_cache__ 做缓存,__webpack_require__ 模拟 require 的行为。这就是为什么打包后的代码不需要浏览器原生支持模块化。

六、输出阶段(Emit)

生成阶段完成后,Webpack 将所有 Asset(文件)写入到 output.path 指定的目录中。

Hook触发时机常见用途
emit写入文件之前修改输出内容、添加额外文件
afterEmit写入文件之后上传 CDN、通知部署系统
done构建全部完成输出构建统计信息、发送通知

七、Loader 机制深入

7.1 Loader 本质

Loader 本质上就是一个 函数,接收源文件内容,返回转换后的内容:

// 一个最简单的 Loader
module.exports = function (source) {
// source 是文件的原始内容(字符串)
const result = source.replace(/console\.log\(.*?\);?/g, '');
return result; // 返回转换后的内容
};

7.2 Loader 的分类

7.3 编写一个自定义 Loader

const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');

// Loader 选项的 JSON Schema
const schema = {
type: 'object',
properties: {
prefix: { type: 'string' },
},
additionalProperties: false,
};

// Loader 函数
module.exports = function (source) {
// 获取配置选项
const options = getOptions(this);
validate(schema, options, { name: 'My Loader' });

// this.callback 可以返回多个结果(如 sourceMap)
// this.async() 可以处理异步逻辑

const prefix = options.prefix || '/* processed */';
return `${prefix}\n${source}`;
};

// Pitch 函数(可选)
module.exports.pitch = function (remainingRequest, precedingRequest, data) {
// 如果 pitch 返回了值,会跳过后续 Loader
// 常见场景:style-loader 用 pitch 来阻断后续执行
};

7.4 常用 Loader 及其作用

Loader作用处理对象
babel-loaderES6+ 语法转换为 ES5.js.jsx
ts-loaderTypeScript 编译.ts.tsx
css-loader解析 CSS 中的 @importurl().css
style-loader将 CSS 注入到 DOM 的 <style> 标签CSS 模块
postcss-loader使用 PostCSS 处理 CSS(如自动加前缀).css
sass-loader编译 Sass/SCSS 为 CSS.scss.sass
file-loader将文件输出到目录并返回 URL图片、字体等
url-loader小文件转 Base64,大文件同 file-loader图片、字体等
raw-loader将文件内容作为字符串导入任意文本文件
thread-loader多线程编译,加速 Loader 执行配合其他 Loader

八、Plugin 机制与 Tapable

8.1 Plugin 的本质

Plugin 是 Webpack 最强大的扩展机制。一个 Plugin 就是一个带 apply 方法的对象,通过 钩子(Hook) 监听 Webpack 构建过程中的特定事件:

class MyPlugin {
// apply 方法在 Webpack 初始化时被调用
apply(compiler) {
// 监听 compilation 钩子
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
console.log('新的 Compilation 创建了!');
});

// 监听 emit 钩子(异步)
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// 在输出文件之前做些事情
const content = '# Build Info\nGenerated at: ' + new Date().toISOString();
compilation.assets['build-info.txt'] = {
source: () => content,
size: () => content.length,
};
callback();
});
}
}

module.exports = MyPlugin;

8.2 Tapable —— Webpack 的事件系统

Webpack 的整个 Hook 机制基于 Tapable 库。Tapable 提供了多种类型的 Hook:

Hook 类型执行方式特点
SyncHook同步串行最基础,不关心返回值
SyncBailHook同步串行遇到返回值非 undefined 即停止
SyncWaterfallHook同步串行上一个回调的返回值传给下一个
AsyncSeriesHook异步串行一个接一个执行
AsyncParallelHook异步并行同时执行所有回调
AsyncSeriesBailHook异步串行遇到返回值即停止

Tapable 使用示例

const { SyncHook, AsyncSeriesHook } = require('tapable');

class Car {
constructor() {
this.hooks = {
// 定义钩子
start: new SyncHook(),
accelerate: new SyncHook(['speed']),
brake: new AsyncSeriesHook(),
};
}
}

const car = new Car();

// 注册钩子回调(类似 addEventListener)
car.hooks.start.tap('Logger', () => {
console.log('汽车启动了');
});

car.hooks.accelerate.tap('Logger', (speed) => {
console.log(`加速到 ${speed} km/h`);
});

// 触发钩子(类似 dispatchEvent)
car.hooks.start.call();
car.hooks.accelerate.call(100);

8.3 Compiler 和 Compilation 的主要 Hook

8.4 常用 Plugin 及其作用

Plugin作用对应 Hook
HtmlWebpackPlugin自动生成 HTML 并注入 Bundle 引用emit
MiniCssExtractPlugin将 CSS 提取为独立文件compilation
DefinePlugin定义全局常量(如环境变量)compilation
CleanWebpackPlugin构建前清空输出目录emit
CopyWebpackPlugin复制静态文件到输出目录emit
TerserPlugin压缩 JS 代码(Webpack 5 内置)optimizeAssets
BundleAnalyzerPlugin可视化分析打包体积done
HotModuleReplacementPlugin启用 HMR 热更新多个 Hook

九、HMR 热模块替换原理

HMR(Hot Module Replacement)是 Webpack 最受欢迎的功能之一,它允许在 不刷新整个页面 的情况下替换、添加或删除模块。

9.1 HMR 架构

9.2 HMR 工作流程详解

9.3 HMR API 使用

// 在模块中声明如何接受更新
if (module.hot) {
// 接受自身更新
module.hot.accept();

// 接受依赖更新,并提供回调
module.hot.accept('./App.js', () => {
// 重新渲染 App 组件
const NextApp = require('./App.js').default;
render(NextApp);
});

// 模块被替换前的清理逻辑
module.hot.dispose((data) => {
// 清理副作用(如定时器、事件监听等)
clearInterval(timer);
// data 对象会传递给新模块
data.state = currentState;
});
}
注意

如果修改的模块及其所有父模块都没有定义 module.hot.accept(),HMR 会退化为 刷新整个页面。React 项目通常使用 react-refresh 来自动处理组件的 HMR。

十、Tree Shaking 原理

Tree Shaking 是 Webpack 用来 移除未使用代码 的优化手段,它依赖 ES Module 的静态结构特性。

10.1 工作原理

10.2 使用条件

Tree Shaking 要生效,需要满足以下条件:

条件说明
ES Module 语法必须使用 import/export,不能用 require/module.exports
production 模式或手动配置 optimization.usedExports: true
无副作用模块的 package.json 设置 "sideEffects": false
静态导入import() 动态导入的模块不参与 Tree Shaking
// math.js —— 模块导出
export const add = (a, b) => a + b; // ✅ 被使用
export const subtract = (a, b) => a - b; // ❌ 未被使用
export const multiply = (a, b) => a * b; // ❌ 未被使用

// index.js —— 只导入了 add
import { add } from './math.js';
console.log(add(1, 2));

// 打包后:subtract 和 multiply 会被移除

10.3 sideEffects 配置

// package.json
{
"name": "my-app",
"sideEffects": false // 声明所有模块都没有副作用
}

// 或者指定有副作用的文件
{
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfills.js"
]
}
什么是副作用?

副作用是指模块在被导入时会执行一些影响外部的操作,如修改全局变量、注册事件监听器、设置 CSS 样式等。如果一个文件只是纯粹的导出函数/常量,没有执行其他逻辑,它就是"无副作用"的。

十一、代码分割(Code Splitting)

代码分割是 Webpack 最重要的性能优化特性之一,它允许将代码按需拆分为多个 Bundle,实现 按需加载

11.1 三种分割方式

11.2 动态导入实现按需加载

// 路由懒加载示例
const routes = [
{
path: '/',
component: () => import(/* webpackChunkName: "home" */ './pages/Home'),
},
{
path: '/about',
component: () => import(/* webpackChunkName: "about" */ './pages/About'),
},
{
path: '/dashboard',
component: () => import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */ // 空闲时预加载
'./pages/Dashboard'
),
},
];

Webpack 魔法注释

注释作用
webpackChunkName自定义 Chunk 名称
webpackPrefetch在浏览器空闲时预加载
webpackPreload与父 Chunk 并行加载
webpackMode控制动态导入的解析模式

11.3 动态加载的运行时原理

当使用 import() 时,Webpack 会在运行时通过 JSONP 动态加载 Chunk:

// Webpack 生成的异步加载代码(简化)
__webpack_require__.e = function (chunkId) {
return new Promise((resolve, reject) => {
// 1. 创建 script 标签
var script = document.createElement('script');
script.src = __webpack_require__.p + chunkId + '.bundle.js';

// 2. 监听加载完成
script.onload = () => {
resolve();
};
script.onerror = () => {
reject(new Error('Loading chunk ' + chunkId + ' failed.'));
};

// 3. 插入到 DOM
document.head.appendChild(script);
});
};

// import('./module') 被编译为:
__webpack_require__
.e('module') // 加载 Chunk
.then(__webpack_require__.bind(null, './module.js')); // 获取模块

十二、Webpack 5 核心新特性

Webpack 5 带来了许多重要的改进:

12.1 持久化缓存(Persistent Caching)

// webpack.config.js
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存(替代内存缓存)
buildDependencies: {
config: [__filename], // 配置文件变化时使缓存失效
},
version: '1.0', // 手动控制缓存版本
},
};

缓存可以将二次构建速度提升 60%-90%

12.2 模块联邦(Module Federation)

// 应用 B:暴露模块(Remote)
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'appB',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./utils': './src/utils',
},
shared: ['react', 'react-dom'],
}),
],
};

// 应用 A:消费模块(Host)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'appA',
remotes: {
appB: 'appB@http://localhost:3001/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
};

12.3 其他重要变化

特性说明
Asset Modules内置资源模块,取代 file-loaderurl-loaderraw-loader
Top Level Await支持模块顶层 await
更好的 Tree Shaking支持嵌套的 Tree Shaking 和内部模块的 Tree Shaking
Node.js Polyfill 移除不再自动注入 Node.js 核心模块的 polyfill
真正的 Content Hash[contenthash] 只在文件内容变化时才改变
更好的长期缓存确定的 Module ID 和 Chunk ID 算法

十三、Webpack 性能优化实践

13.1 构建速度优化

// 构建速度优化配置示例
module.exports = {
// 1. 开启持久化缓存
cache: { type: 'filesystem' },

module: {
// 2. 跳过大型库的解析
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/,
// 3. 缩小编译范围
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/,
use: [
// 4. 多线程编译
{ loader: 'thread-loader', options: { workers: 4 } },
'babel-loader',
],
},
],
},

resolve: {
// 5. 减少文件搜索范围
modules: [path.resolve(__dirname, 'node_modules')],
extensions: ['.js', '.jsx'], // 避免过多扩展名
},

// 6. 不打包某些大型库
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
};

13.2 产出体积优化

手段配置效果
代码压缩TerserPlugin(Webpack 5 默认启用)JS 体积减少 40%-60%
Tree Shakingmode: 'production' + ES Module移除未使用代码
代码分割splitChunks + 动态 import()按需加载、减少首屏体积
CSS 提取MiniCssExtractPlugin分离 CSS,支持缓存
图片压缩image-webpack-loader图片体积减少 30%-70%
Gzip 压缩compression-webpack-plugin传输体积减少 60%-80%
Scope Hoistingoptimization.concatenateModules减少模块包装代码

13.3 Scope Hoisting(作用域提升)

Scope Hoisting 是 Webpack 3 引入的优化,它将模块内联到一个函数作用域中,减少函数声明和闭包的开销:

// 优化前:每个模块都被包在一个函数中
/* 0 */
(function (module, exports) {
const name = 'world';
module.exports = name;
});
/* 1 */
(function (module, exports, __webpack_require__) {
const name = __webpack_require__(0);
console.log('hello ' + name);
});

// Scope Hoisting 优化后:模块合并到同一作用域
(() => {
const name = 'world';
console.log('hello ' + name);
})();

十四、Webpack 完整工作流程图

把所有阶段串起来,这就是 Webpack 的完整工作流程:

十五、面试高频问答

Q1:Webpack 的构建流程是怎样的?

:Webpack 的构建流程分为三大阶段:

  1. 初始化阶段:读取并合并配置(命令行参数 > 配置文件 > 默认值),创建 Compiler 对象,注册所有 Plugin。
  2. 构建阶段(Make):从 Entry 出发,对每个模块调用对应的 Loader 进行转换,然后使用 JavascriptParser(基于 acorn)解析为 AST,遍历 AST 收集依赖(importrequire 等),递归处理所有依赖模块,最终构建出完整的依赖图(Dependency Graph)
  3. 生成阶段(Seal):根据依赖图和配置策略将模块组装为 Chunk,然后对 Chunk 进行优化(Tree Shaking、Scope Hoisting、代码压缩等),生成最终的 Bundle 代码并输出到文件系统。

Q2:Loader 和 Plugin 的区别是什么?

  • Loader 是文件转换器,本质是一个函数,将非 JS 文件(如 CSS、图片、TypeScript)转换为 Webpack 能处理的模块。它工作在模块构建阶段,遵循从右到左、从下到上的执行顺序。
  • Plugin 是功能扩展器,本质是一个带 apply 方法的对象。它通过监听 Webpack 构建过程中的 Tapable Hook 来介入整个生命周期,能力更广泛,可以做打包优化、资源管理、环境变量注入等任何事情。

简单说:Loader 做翻译,Plugin 做增强

Q3:Webpack 的热更新(HMR)原理是什么?

:HMR 的核心流程:

  1. Webpack Dev Server 监听文件变化,触发增量编译。
  2. 编译完成后生成两个文件:hot-update.json(更新清单)和 hot-update.js(更新代码)。
  3. Dev Server 通过 WebSocket 向浏览器推送更新通知(包含新的 hash 值)。
  4. 浏览器端的 HMR Runtime 收到通知后,通过 JSONP 请求下载更新文件。
  5. HMR Runtime 对比新旧模块,调用 module.hot.accept() 注册的回调来应用更新。
  6. 如果没有模块接受更新,则向上冒泡直至刷新整个页面。

Q4:Tree Shaking 的原理和使用条件是什么?

:Tree Shaking 基于 ES Module 的 静态结构 特性(import/export 在编译时就可以确定引用关系,不需要运行代码)。Webpack 在构建阶段会分析模块的导出使用情况(usedExports),标记未被使用的导出,然后在压缩阶段由 Terser 将这些死代码移除。

使用条件:

  • 必须使用 ES Module 语法(import/export),CommonJS 的动态特性导致无法静态分析
  • 开启 mode: 'production' 或手动设置 optimization.usedExports: true
  • package.json 中标记 "sideEffects": false(或指定有副作用的文件),帮助 Webpack 安全移除未使用的模块

Q5:Webpack 的 Chunk 是怎么生成的?有哪几种类型?

:Chunk 是 Webpack 打包的中间产物,由一组模块组成。Chunk 的生成有三种方式:

  1. Entry Chunk:每个入口(entry)会生成一个初始 Chunk,包含入口模块及其同步依赖。
  2. Async Chunk:代码中使用 import() 动态导入时,被导入的模块会被单独拆分为一个异步 Chunk,实现按需加载。
  3. SplitChunks:通过 optimization.splitChunks 配置,Webpack 会自动将符合条件的公共模块(如多次引用的模块、node_modules 中的第三方库)提取到单独的 Chunk 中。

此外还有 Runtime Chunk,通过 optimization.runtimeChunk 可以将 Webpack 的运行时代码(__webpack_require__ 等)单独提取。

Q6:Webpack 中如何优化构建速度?

:构建速度优化可以从以下几个维度入手:

  1. 缓存:开启 cache: { type: 'filesystem' } 持久化缓存,二次构建速度提升 60%-90%。
  2. 缩小搜索范围:配置 include/exclude 限制 Loader 处理范围,精简 resolve.extensionsresolve.modules
  3. 多线程:使用 thread-loader 将耗时的 Loader(如 babel-loader)放到 Worker 线程中执行。
  4. 跳过解析:使用 module.noParse 跳过不需要解析依赖的大型库(如 jQuery、Lodash)。
  5. 外部化:使用 externals 将大型库排除,通过 CDN 引入。
  6. DLL:使用 DllPlugin 预先编译不常变化的第三方库(Webpack 5 的持久化缓存已大部分替代了这个方案)。

Q7:Webpack 的 Module Federation 是什么?解决了什么问题?

:Module Federation(模块联邦)是 Webpack 5 引入的重要特性,它允许 多个独立构建的应用在运行时共享模块

它主要解决了微前端架构中的以下问题:

  • 代码共享:不同应用可以在运行时共享组件和库,避免重复打包。
  • 独立部署:每个应用可以独立构建和部署,不需要重新构建消费方。
  • 版本管理:通过 shared 配置自动处理依赖版本协调。

核心概念:Host(消费者)通过远程加载 Remote(提供者)暴露的模块,实现跨应用的模块共享,且共享的公共依赖(如 React)只会加载一份。

Q8:Compiler 和 Compilation 的区别是什么?

  • Compiler:代表整个 Webpack 构建的 全局实例,在 Webpack 启动时创建,贯穿整个构建生命周期。一次 Webpack 运行只有一个 Compiler 实例,它保存了完整的配置信息、所有插件引用和文件系统访问能力。
  • Compilation:代表 一次具体的编译过程。每当文件变化触发重新编译时,都会创建一个新的 Compilation 实例。它包含了当前编译的模块资源、Chunk、生成的 Asset 等信息。

类比:Compiler 是一家 工厂,而 Compilation 是工厂里的 一次生产批次。工厂只有一个,但可以反复生产。在 watch 模式下,每次文件变化都会创建新的 Compilation,而 Compiler 始终是同一个。