Webpack 构建流程与性能优化详解

Webpack 的构建流程是什么?

第一阶段:初始化

  1. 合并配置参数:读取配置文件和命令行参数,合并为最终的配置对象。
  2. 判断配置类型: 如果传入的是数组,Webpack 会创建 MultiCompiler;如果是对象,则创建单个 Compiler。多配置常用于需要同时打包客户端和服务端代码的场景。
  3. 创建 Compiler 对象,初始化插件:根据合并后的配置参数创建 Compiler 对象。遍历插件(是一个class)的 apply 方法并传递 compiler 实例,把插件回调注册到对应的钩子上。

第二阶段:编译

  1. 调用 run 方法,开始编译: 调用 compiler.run() 后,Webpack 会依次触发 beforeRunrun 这两个钩子,然后进入真正的编译流程。
  2. 创建 Compilation 对象: 每次编译都会创建一个新的 Compilation 对象。Compilation 会存储当前这次打包涉及的所有模块、依赖关系、最终资源等信息。
  3. 触发 make 钩子,开始构建make 钩子触发后,SingleEntryPlugin 等会调用 compilation.addEntry,把入口文件添加到构建队列中,开始递归构建依赖图。
  4. 编译模块:从入口文件出发,对每个模块执行以下步骤:
    • 执行 Loader 链:通过 doBuild 方法,按照配置的 Loader 顺序(从右向左)处理模块内容,将非 JS 文件转换为 JS 模块。
    • AST 解析提取依赖:执行完 Loader 后,Webpack 会将代码转换成 AST(抽象语法树),然后遍历 AST,找出所有的 importrequire 等语句,提取出依赖的模块路径。
    • 递归处理依赖:对提取出的每个依赖,递归调用 buildModule,重复上面两个步骤,直到所有依赖都被处理完。
    • 每个模块构建前后会触发 buildModulesucceedModule 钩子。这里有一个关键设计:模块缓存,已经构建过的模块会被缓存起来,避免循环依赖导致的死循环,也避免重复构建。
  5. 完成模块编译:所有模块构建完成后触发 finishModules 钩子,此时得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 封装阶段(seal):进入 seal 阶段,Webpack 会做三件事:
    • 优化:触发 optimize 系列钩子,进行 Tree Shaking(删除未使用的导出)、模块合并等优化。
    • 分块:根据 splitChunks 配置,将模块重新组合成 Chunk
    • 生成资源:将每个 Chunk 转换成最终的文件内容(字符串),存储到 compilation.assets 中。
  7. 输出资源:触发 emit 钩子,这是插件修改输出内容的最后机会。插件可以在这里遍历 compilation.assets,修改、添加或删除资源。然后根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
  8. 输出完成:写入完成后触发 done 钩子,整个打包流程结束。

Tree Shaking和SideEffects

Tree Shaking 通过静态分析 ES Module 的导入导出关系,在打包时移除未被引用的代码。

sideEffectspackage.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打包速度?

  1. 减少工作范围
  • include/exclude:缩小 loader 处理范围(如 include: path.resolve('src')
  • resolve 优化extensions 常用后缀放前面,alias 减少查找路径
  • oneOf:每个文件只匹配第一个命中规则,没有 oneOf 会遍历所有规则进行匹配
  • 移除无用插件:生产环境去掉开发插件
  1. 使用缓存,避免重复
  • 持久化缓存(Webpack 5):cache.type: 'filesystem',二次构建极快
  • loader 缓存babel-loader 开启 cacheDirectory
  1. 并行构建
  • thread-loader:耗时 loader 放入多进程池(注意:小型项目不要用,进程启动有开销)
  • TerserPluginparallel: true 开启多进程压缩
  1. 升级工具链
  • 升级版本:Webpack 5 比 4 快 20-30%
  • esbuild-loader / swc-loader:可用 Rust/Go 实现的 loader 替代 babel,速度提升 10-100 倍

如何减小Webpack打包后的体积/性能优化?

  1. 代码分割(排除掉非首屏加载需要的数据)
    • 入口分割:多入口配置,各自打包独立文件
    • 动态导入import() 实现路由懒加载,非首屏代码单独打包
    • splitChunks:提取公共模块(如 react、vue)和第三方库到独立 chunk,避免重复打包
  2. Tree Shaking(移除未使用代码)
    • 前提:使用 ES Module(import/export
    • 配置optimization.usedExports: true 开启标记
    • 副作用package.json 中设置 sideEffects: false 或列出有副作用文件(如 "*.css"
    • Babelpreset-env 需设置 modules: false 保留 ES Module 语法(否则Tree Shaking会失效)
  3. 代码压缩
    • JS 压缩TerserPlugin(Webpack 5 默认,mode: production 自动开启)
    • CSS 压缩CssMinimizerPlugin
    • HTML 压缩HtmlWebpackPlugin 在生产环境默认开启,
  4. 图片/资源优化
    • 图片压缩image-minimizer-webpack-plugin(配合 imagemin、squoosh、sharp)
    • 资源模块type: 'asset' 配合 parser.dataUrlCondition.maxSize 小图转 base64
    • 字体/SVG 优化:按需加载或使用 url-loader 限制大小
  5. 按需引入(减少依赖体积)
    • babel polyfilluseBuiltIns: 'usage' 按需引入,避免全量 polyfill
    • 第三方库按需:lodash 用 lodash-es,antd/element-ui 配置 babel-plugin-import
    • 避免全量引入import { debounce } from 'lodash-es' 而非 import _ from 'lodash'
  6. CDN 加速(外部化)
    • 配置externals 将 react、vue 等排除打包,通过 CDN 引入
    • 好处:减小 bundle 体积,利用 CDN 缓存和并行加载能力
  7. 其它优化
    • gzip 压缩:compression-webpack-plugin 生成 .gz 文件,配合服务端配置
    • Scope Hoisting(作用域提升):optimization.concatenateModules: true 合并模块作用域
    • 移除开发代码mode: production 自动移除 console.logdebugger
    • 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 文件,处理 @importurl()
  • 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
      ]
    }
  ]
}

执行流程

  1. sass-loader:将 .scss 文件编译为 CSS
  2. css-loader:解析 CSS 中的 @importurl()
  3. 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.jsloader-runner 中,Loader 的执行逻辑遵循从右到左的顺序。简化后的执行逻辑如下:

  1. loaders 数组按配置顺序存储
  2. 执行时从数组末尾开始(i = loaders.length - 1
  3. 每次执行后 i-- 向左移动
  4. 前一个 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

五、设计好处

  1. 符合直觉:原始文件 → 中间转换 → 最终结果
  2. 易于理解:管道式处理,每个 Loader 只关注自己的输入输出
  3. 灵活组合:可以轻松插入新的 Loader 到合适位置
  4. 职责分离:每个 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 链,避免常见的配置错误。

源码参考链接:loader-runnerwebpack/lib/NormalModule.js

← 返回列表