Skip to content

Webpack 原理与优化

构建流程 · Loader/Plugin · 文件指纹 · HMR · 构建提速 · 分片优化 · 路由懒加载


一、Webpack 构建流程

  1. 初始化参数(合并配置)
  2. 创建 Compiler 对象,注册插件
  3. 从 entry 入口解析文件构建 AST,找出依赖
  4. 递归调用 Loader 编译模块
  5. 组装 Chunk,生成输出资源
  6. 写入文件系统

Compiler vs Compilation

  • Compiler:webpack 启动时一次性创建,包含完整配置环境(options/loaders/plugins),整个构建生命周期只存在一个
  • Compilation:一次资源版本的构建,开发模式下文件变化会创建新的 Compilation

二、Loader 与 Plugin 区别

  • Loader:文件转换器,运行在打包前,将 A 文件编译为 B 文件(如 scss → css)
  • Plugin:扩展 webpack 功能,在整个编译周期起作用,监听事件广播执行任务
  • 常用 Loader:style-loader、css-loader、less-loader、babel-loader、file-loader、url-loader

自定义 Loader 实现

Loader 本质:一个导出函数的 JS 模块,接收源文件内容(source),返回编译后的内容

js
// replace-loader.js
const { getOptions } = require('loader-utils')
module.exports = function(source) {
  const options = getOptions(this)  // 获取 webpack 配置中的 options
  return source.replace(/NAME/g, options.words)
}

Loader 调用顺序(pitch 机制)

  • 配置 use: ['style-loader', 'css-loader', 'sass-loader']
  • 先从左往右执行各 loader 的 pitch 方法,再从右往左执行 loader 主函数
  • 若某个 pitch 返回值,则中断后续调用,反向执行前面的 loader
style-loader(pitch) → css-loader(pitch) → sass-loader(pitch)

                    style-loader ← css-loader ← sass-loader(主函数)

Loader 常用 API

  • this.async():异步 loader,返回 callback
  • this.cacheable(false):禁用缓存(默认缓存)
  • loader.raw = true:处理二进制数据(Buffer)
  • getOptions(this):获取配置传入的 options

mini style-loader 原理(pitch 用法):

  • pitch 阶段返回一段 JS 代码字符串
  • 这段代码创建 <style> 标签,通过 require(!!remainingRequest) 获取 css-loader 处理后的内容
  • !! 前缀表示跳过所有 loader 直接 require
  • 将 CSS 内容插入 <style> 标签并 append 到 <head>

自定义 Plugin 实现

Plugin 组成

  1. 一个 JavaScript 命名函数
  2. 在原型上定义 apply 方法
  3. 注册 webpack 的事件钩子(Tapable)
  4. 处理 webpack 内部实例的特定数据
  5. 功能完成后调用 webpack 提供的回调

自定义 Plugin 示例(生成构建文件列表):

js
class FileListPlugin {
  constructor(options) {
    this.filename = options.filename || 'filelist.md'
  }
  apply(compiler) {
    // 注册 emit 钩子(输出文件前触发)
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
      const files = Object.keys(compilation.assets)
      let content = `${files.length} files in this build:\n\n`
      files.forEach(f => { content += `* ${f}\n` })
      // 将新资源插入构建结果
      compilation.assets[this.filename] = {
        source: () => content,
        size: () => content.length
      }
      cb()
    })
  }
}
module.exports = FileListPlugin

三、文件指纹策略

  • hash:以项目为单位,项目内容变则 hash 变
  • chunkhash:以 chunk 为单位,对应 chunk 内文件变则 hash 变
  • contenthash:以自身内容为单位
  • 推荐:CSS 用 contenthash,JS 用 chunkhash

四、Webpack 热更新(HMR)原理

  1. webpack-dev-server 创建两个服务:静态资源服务(express)+ Socket 服务(websocket)
  2. 文件变化时,Socket 服务生成 manifest.json + update chunk.js
  3. 通过 websocket 推送给浏览器
  4. 浏览器通过 HMR runtime 加载更新模块

五、提升 Webpack 构建速度

  • 缩小 loader 处理范围:用 include/exclude 限定范围,避免处理 node_modules
    js
    { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ }
  • resolve.alias:直接指定模块路径,跳过递归搜索
  • module.noParse:跳过无依赖库的解析(如 jQuery 不依赖其他包)
    js
    module: { noParse: /jQuery/ }
  • IgnorePlugin:忽略第三方包的指定目录(如 moment.js 的 locale 文件)
    js
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
    // moment 包体积从 263KB 降至 55KB
  • 使用 DLLPlugin 预编译不常变的库
  • 使用 cache-loader / 构建缓存
  • terser 多线程压缩
  • 生产环境关闭 SourceMap:productionSourceMap: false

六、分片优化(splitChunks 实战)

分片原则

  1. node_modules 中的文件打包进 chunk-vendors,变动频率低,可利用 304 缓存
  2. 被引用少且体量大的库单独分一个 chunk(如 echarts、tinymce)
  3. 公共代码分成多个 chunk,避免从某入口访问时下载全部公共代码
  4. 体量很小的异步 chunk 合并进相关 chunk 或入口 chunk

路由懒加载决策矩阵

使用频率高使用频率低
文件小合并进入口 chunk单独一个 chunk
文件大单独一个 chunk(加 prefetch)单独一个 chunk

cacheGroups 配置示例

js
splitChunks: {
  maxInitialRequests: 5,
  maxAsyncRequests: 6,
  cacheGroups: {
    vendors: {
      name: 'chunk-vendors',
      test: /[\\/]node_modules[\\/]/,
      priority: -10,
      minChunks: 2,
      chunks: 'all',
    },
    echarts: {
      name: 'chunk-echarts',
      test: /[\\/]node_modules[\\/]echarts[\\/]/,
      priority: 0,
      chunks: 'all',
    },
    common: {
      name: true,
      minChunks: 2,
      minSize: 60000,
      priority: -20,
      chunks: 'initial',
      reuseExistingChunk: true,
    },
  },
}

chunk-vendors vs chunk-common 策略不同:vendors 是第三方库,变动少可用 304 长期缓存;common 是项目代码,经常变动缓存易失效,因此 common 应拆分而非合并


七、路由懒加载

js
{
  path: '/user/XXX',
  component: () => import(/* webpackChunkName: "chunk-user-XXX" */ 'src/pages/XXX'),
}

路由懒加载原理(Webpack JSONP 机制)

import() 编译后,Webpack 生成以下核心机制:

  1. window["webpackJsonp"]:全局数组,各 chunk 通过 .push() 注册自身模块
  2. webpackJsonpCallback:拦截 push 方法,将 chunk 中的模块缓存到 modules 对象,并更新 installedChunks[chunkId] = 0(标记已加载)
  3. __webpack_require__(moduleId):执行模块,带缓存(installedModules)避免重复执行
  4. __webpack_require__.e(chunkId)(核心):通过 JSONP 动态加载 chunk
    • 创建 <script> 标签,src 指向对应 chunk 文件
    • 三种加载状态:0(已完成)、undefined(未加载/失败/超时)、Promise(加载中)
    • 超时处理:120s 超时后 reject Promise,抛出 ChunkLoadError

完整流程

用户访问路由 → import() 触发 → webpackAsyncContext 查 map 找到 chunkId
→ __webpack_require__.e(chunkId) 创建 <script> 加载 JS
→ chunk 加载完成执行 webpackJsonpCallback → 模块注册到 modules
→ __webpack_require__(moduleId) 执行模块 → 渲染页面

八、预加载策略(Preload / Prefetch)

  • preload:告诉浏览器当前页面马上要用到的文件,高优先级下载
  • prefetch:告诉浏览器空闲时提前下载用户接下来可能用到的文件
  • 策略:preload chunk-vendors + 入口 chunk;prefetch 常用 asyncChunks
  • 注意:preload 要放在 prefetch 之前;prefetch 不应滥用(占用带宽)
  • 如果 <script>crossorigin 属性,prefetch/preload 也必须有,否则资源会重复下载

相关