Webpack 构建流程与性能优化详解
Webpack 的构建流程是什么?
第一阶段:初始化
- 合并配置参数:读取配置文件和命令行参数,合并为最终的配置对象。
- 判断配置类型: 如果传入的是数组,Webpack 会创建
MultiCompiler;如果是对象,则创建单个Compiler。多配置常用于需要同时打包客户端和服务端代码的场景。 - 创建 Compiler 对象,初始化插件:根据合并后的配置参数创建
Compiler对象。遍历插件(是一个class)的apply方法并传递compiler实例,把插件回调注册到对应的钩子上。
第二阶段:编译
- 调用 run 方法,开始编译: 调用
compiler.run()后,Webpack 会依次触发beforeRun和run这两个钩子,然后进入真正的编译流程。 - 创建 Compilation 对象: 每次编译都会创建一个新的
Compilation对象。Compilation会存储当前这次打包涉及的所有模块、依赖关系、最终资源等信息。 - 触发 make 钩子,开始构建:
make钩子触发后,SingleEntryPlugin等会调用compilation.addEntry,把入口文件添加到构建队列中,开始递归构建依赖图。 - 编译模块:从入口文件出发,对每个模块执行以下步骤:
- 执行 Loader 链:通过
doBuild方法,按照配置的 Loader 顺序(从右向左)处理模块内容,将非 JS 文件转换为 JS 模块。 - AST 解析提取依赖:执行完 Loader 后,Webpack 会将代码转换成 AST(抽象语法树),然后遍历 AST,找出所有的
import、require等语句,提取出依赖的模块路径。 - 递归处理依赖:对提取出的每个依赖,递归调用
buildModule,重复上面两个步骤,直到所有依赖都被处理完。 - 每个模块构建前后会触发
buildModule和succeedModule钩子。这里有一个关键设计:模块缓存,已经构建过的模块会被缓存起来,避免循环依赖导致的死循环,也避免重复构建。
- 执行 Loader 链:通过
- 完成模块编译:所有模块构建完成后触发
finishModules钩子,此时得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。 - 封装阶段(seal):进入
seal阶段,Webpack 会做三件事:- 优化:触发
optimize系列钩子,进行 Tree Shaking(删除未使用的导出)、模块合并等优化。 - 分块:根据
splitChunks配置,将模块重新组合成Chunk。 - 生成资源:将每个
Chunk转换成最终的文件内容(字符串),存储到compilation.assets中。
- 优化:触发
- 输出资源:触发
emit钩子,这是插件修改输出内容的最后机会。插件可以在这里遍历compilation.assets,修改、添加或删除资源。然后根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 - 输出完成:写入完成后触发
done钩子,整个打包流程结束。
Tree Shaking和SideEffects
Tree Shaking 通过静态分析 ES Module 的导入导出关系,在打包时移除未被引用的代码。
sideEffects 是 package.json 中的配置项,用于告诉 Webpack 模块是否有副作用(模块在被导入时,除了导出的内容,其它代码执行的一些操作,例如修改全局变量、原型等),从而决定打包时哪些代码可以安全删除。如果设置为true,无论是否被引用,模块代码都会被打包。
| 概念 | 本质 | 作用 |
|---|---|---|
| Tree Shaking | 优化技术 | 删除未使用的导出 |
| sideEffects | 配置项 | 声明代码是否有副作用,影响删除策略 |
Scope Hoisting 作用域提升
Webpack 默认会将每个模块包裹在独立的函数闭包中,导致大量函数作用域开销。作用域提升通过 optimization.concatenateModules: true 开启(生产环境默认启用),其核心逻辑是由内置的 ModuleConcatenationPlugin 插件实现的,它能将多个相互引用的 ES Module 直接合并到同一个函数作用域中。这样做不仅消除了模块包裹函数的调用开销,让代码执行更快,还因为减少了大量 __webpack_require__ 代码和闭包而显著减小了打包体积,同时给 Terser 等压缩工具提供了更大的优化空间。不过它的前提是模块必须使用 ESM 语法且无循环依赖。
Tree Shaking原理
Tree Shaking 的本质是删除未引用的代码,它依赖于 ES Module 的静态语法特性。通过 optimization.usedExports: true 开启标记功能,Webpack 会在编译时标记出未使用的导出(unused harmony export),再由 Terser 等压缩工具在压缩阶段执行删除操作。
副作用 指的是模块在被导入时,即使没有被使用,也会执行并影响环境的代码(如 console.log、修改全局变量、引入 CSS 文件等)。由于 Webpack 无法静态分析出这些代码是否安全,它会保守地保留所有模块。为了让 Tree Shaking 更激进,需要在 package.json 中设置 sideEffects: false(承诺所有模块无副作用)或精确列出有副作用的文件路径(如 "*.css"),这样 Webpack 才能安全地删除那些未被使用且无副作用的模块。
Module、Chunk、Bundle 区别
Module 是开发时概念,每个源文件是一个module(.js、.css、.png 等),是 Webpack 处理的基本单位。
Chunk 是 Webpack 构建过程中的中间分组,根据入口、动态导入(import())、代码分割规则(splitChunks)将多个 Module 组合成不同的 Chunk。每个 Chunk 代表一个代码块,可能是首屏要用的,也可能是异步加载的。
Bundle 是最终输出的文件,每个 Chunk 经过优化、压缩后生成一个 Bundle。
关系:一个入口默认生成一个 Chunk,再输出为一个 Bundle(例外:MiniCssExtractPlugin插件会从 Chunk 中提取 CSS 代码,为其创建一个全新的、独立的 Chunk,然后基于这个新 Chunk 生成一个独立的 Bundle);通过动态导入或 splitChunks,一个入口可以产生多个 Chunk,进而输出多个 Bundle。
如何提高Webpack打包速度?
- 减少工作范围
include/exclude:缩小 loader 处理范围(如include: path.resolve('src'))resolve优化:extensions常用后缀放前面,alias减少查找路径oneOf:每个文件只匹配第一个命中规则,没有oneOf会遍历所有规则进行匹配- 移除无用插件:生产环境去掉开发插件
- 使用缓存,避免重复
- 持久化缓存(Webpack 5):
cache.type: 'filesystem',二次构建极快 - loader 缓存:
babel-loader开启cacheDirectory
- 并行构建
thread-loader:耗时 loader 放入多进程池(注意:小型项目不要用,进程启动有开销)- TerserPlugin:
parallel: true开启多进程压缩
- 升级工具链
- 升级版本:Webpack 5 比 4 快 20-30%
esbuild-loader/swc-loader:可用 Rust/Go 实现的 loader 替代 babel,速度提升 10-100 倍
如何减小Webpack打包后的体积/性能优化?
- 代码分割(排除掉非首屏加载需要的数据)
- 入口分割:多入口配置,各自打包独立文件
- 动态导入:
import()实现路由懒加载,非首屏代码单独打包 splitChunks:提取公共模块(如 react、vue)和第三方库到独立 chunk,避免重复打包
- Tree Shaking(移除未使用代码)
- 前提:使用 ES Module(
import/export) - 配置:
optimization.usedExports: true开启标记 - 副作用:
package.json中设置sideEffects: false或列出有副作用文件(如"*.css") - Babel:
preset-env需设置modules: false保留 ES Module 语法(否则Tree Shaking会失效)
- 前提:使用 ES Module(
- 代码压缩
- JS 压缩:
TerserPlugin(Webpack 5 默认,mode: production自动开启) - CSS 压缩:
CssMinimizerPlugin - HTML 压缩:
HtmlWebpackPlugin在生产环境默认开启,
- JS 压缩:
- 图片/资源优化
- 图片压缩:
image-minimizer-webpack-plugin(配合 imagemin、squoosh、sharp) - 资源模块:
type: 'asset'配合parser.dataUrlCondition.maxSize小图转 base64 - 字体/SVG 优化:按需加载或使用
url-loader限制大小
- 图片压缩:
- 按需引入(减少依赖体积)
- babel polyfill:
useBuiltIns: 'usage'按需引入,避免全量 polyfill - 第三方库按需:lodash 用
lodash-es,antd/element-ui 配置 babel-plugin-import - 避免全量引入:
import { debounce } from 'lodash-es'而非import _ from 'lodash'
- babel polyfill:
- CDN 加速(外部化)
- 配置:
externals将 react、vue 等排除打包,通过 CDN 引入 - 好处:减小 bundle 体积,利用 CDN 缓存和并行加载能力
- 配置:
- 其它优化
gzip压缩:compression-webpack-plugin生成 .gz 文件,配合服务端配置Scope Hoisting(作用域提升):optimization.concatenateModules: true合并模块作用域- 移除开发代码:
mode: production自动移除console.log、debugger DefinePlugin:替换环境变量,剔除开发分支代码(如if (process.env.NODE_ENV !== 'production'))
常见的Loader Plugin有哪些?
Webpack 的生态丰富,Loader 和 Plugin 是其核心扩展机制。以下分类介绍常见的 Loader 和 Plugin:
Loader(文件转换器)
Loader 负责将非 JavaScript 模块转换为 Webpack 能够处理的模块。
1. JavaScript/TypeScript 相关
- babel-loader:将 ES6+/TypeScript 转换为浏览器兼容的 JavaScript
- ts-loader:TypeScript 编译(也可用 babel-loader + @babel/preset-typescript)
- swc-loader / esbuild-loader:Rust/Go 实现的超快编译工具
2. 样式处理
- css-loader:解析 CSS 文件,处理
@import和url() - style-loader:将 CSS 注入到 DOM 中(开发环境常用)
- sass-loader / less-loader:编译 Sass/Less 为 CSS
- postcss-loader:使用 PostCSS 处理 CSS(自动添加前缀等)
3. 资源文件
- file-loader:将文件复制到输出目录并返回 URL
- url-loader:小文件转 base64,大文件回退到 file-loader
- image-webpack-loader:图片压缩优化
- svg-url-loader:SVG 文件优化
4. 其他
- markdown-loader:将 Markdown 转换为 HTML(本项目自定义实现)
- vue-loader:Vue 单文件组件处理
- graphql-loader:GraphQL 查询文件处理
Plugin(构建过程扩展)
Plugin 在 Webpack 构建生命周期中执行更广泛的任务。
1. HTML 相关
- HtmlWebpackPlugin:生成 HTML 文件,自动注入打包后的资源
- HtmlMinifierPlugin:HTML 压缩(本项目服务端使用)
2. CSS 处理
- MiniCssExtractPlugin:提取 CSS 为独立文件(生产环境必备)
- CssMinimizerPlugin:CSS 压缩优化
3. 代码优化
- TerserPlugin:JavaScript 压缩(Webpack 5 默认)
- CompressionWebpackPlugin:生成 gzip/brotli 压缩文件
- BundleAnalyzerPlugin:可视化分析打包体积
4. 开发体验
- HotModuleReplacementPlugin:热模块替换
- ReactRefreshWebpackPlugin:React 组件热更新(保留状态)
- ForkTsCheckerWebpackPlugin:TypeScript 类型检查(独立进程)
5. 环境变量与代码注入
- DefinePlugin:定义全局常量(编译时替换)
- ProvidePlugin:自动加载模块,无需显式 import
- BannerPlugin:为文件添加头部注释
6. 代码分割与性能
- SplitChunksPlugin:代码分割(Webpack 4+ 内置)
- LoadableWebpackPlugin:配合 @loadable/component 实现 SSR 代码分割
- WebpackManifestPlugin:生成资源清单文件
为什么Loader执行顺序是从右到左?
Webpack 配置中 Loader 的执行顺序遵循 从右到左(或从下到上) 的规则,这与函数式编程中的 函数组合(function composition) 模式一致。
一、基本规则
// Webpack 配置示例
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader', // 3. 最后执行:将 CSS 注入 DOM
'css-loader', // 2. 然后执行:解析 CSS 依赖
'sass-loader' // 1. 最先执行:编译 Sass 为 CSS
]
}
]
}
执行流程:
sass-loader:将.scss文件编译为 CSScss-loader:解析 CSS 中的@import和url()style-loader:将最终 CSS 注入到页面中
二、设计原理:函数组合模式
Loader 链可以看作是一系列函数的组合:
// 函数组合:f(g(x)) = compose(f, g)(x)
const compose = (f, g) => (x) => f(g(x));
// Loader 链的等价表示
const processSCSS = compose(
styleLoader, // 最后执行
compose(
cssLoader, // 中间执行
sassLoader // 最先执行
)
);
const result = processSCSS(scssSource);
这种设计让 Loader 的职责清晰:
- 右侧 Loader:处理原始文件,进行初步转换
- 左侧 Loader:处理转换后的结果,进行最终处理
三、源码角度分析
在 Webpack 的 NormalModule.js 和 loader-runner 中,Loader 的执行逻辑遵循从右到左的顺序。简化后的执行逻辑如下:
loaders数组按配置顺序存储- 执行时从数组末尾开始(
i = loaders.length - 1) - 每次执行后
i--向左移动 - 前一个 Loader 的输出作为后一个 Loader 的输入
四、实际项目例子
1. CSS Modules 处理流程
{
test: /\.module\.scss$/,
use: [
'style-loader', // 4. 注入样式
{
loader: 'css-loader', // 3. 生成 CSS Modules 类名映射
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
},
'postcss-loader', // 2. 自动添加前缀等
'sass-loader' // 1. 编译 Sass
]
}
2. TypeScript + Babel 处理
{
test: /\.tsx?$/,
use: [
'babel-loader', // 2. Babel 转译(支持新语法)
'ts-loader' // 1. TypeScript 类型检查和编译
]
}
// 注意:也可以只用 babel-loader + @babel/preset-typescript
五、设计好处
- 符合直觉:原始文件 → 中间转换 → 最终结果
- 易于理解:管道式处理,每个 Loader 只关注自己的输入输出
- 灵活组合:可以轻松插入新的 Loader 到合适位置
- 职责分离:每个 Loader 功能单一,易于维护
六、记忆技巧
- 从右到左:配置数组中的顺序
- 从下到上:在
use数组中的位置 - 管道思想:
source → loader1 → loader2 → ... → final
七、特殊情况
1. enforce 属性
可以通过 enforce: 'pre' 或 enforce: 'post' 强制 Loader 在特定阶段执行:
{
test: /\.js$/,
enforce: 'pre', // 最先执行
loader: 'eslint-loader'
},
{
test: /\.js$/,
enforce: 'post', // 最后执行
loader: 'my-custom-loader'
}
执行顺序:pre → normal → inline → post
2. oneOf 规则
在 oneOf 数组中,只有第一个匹配的规则会生效:
{
oneOf: [
{
test: /\.module\.css$/,
use: ['style-loader', 'css-loader?modules'] // 匹配此规则后停止
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'] // 不匹配 .module.css 的文件
}
]
}
八、总结
Loader 的从右到左执行顺序是 Webpack 的核心设计之一,它遵循函数组合模式,让数据处理流程清晰,支持灵活的管道式处理,符合开发直觉。理解这一设计有助于正确配置 Loader 链,避免常见的配置错误。