webpack代码分割

5 阅读6分钟

代码分割

代码分离是 webpack 中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle 中,然后便能按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle、控制资源加载优先级,如果使用合理,会极大减小加载时间。

为什么要代码分割

如果不进行代码分割,webpack 会将所有的依赖打包进一个巨大的 bundle.js 中,这会导致以下问题:

  • 首次加载时间过长
  • 缓存失效频率高

代码分割实现了这些功能:

  • 按需加载:只有在需要的时候才加载代码,可以减少首次加载时间。
  • 并行加载: 将代码拆分成多个小文件,可以利用浏览器的多线程并行下载,提高加载速度。
  • 缓存利用:将不同的代码分割到不同的文件中,可以利用浏览器的缓存机制,减少重复加载。

代码分离

常用的代码分离方法有三种:

  • 入口起点:使用 entry 配置手动地分离代码。
  • 防止重复:使用 入口依赖 或者 SplitChunksPlugin 去重和分离 chunk。
  • 动态导入:通过模块的内联函数调用分离代码。

方法一:多入口起点-最基础的手动配置

通过配置entry属性,手动指定多个入口起点,每个入口起点会生成一个独立的 bundle 文件。

module.exports = {
  entry: {
    main: "./src/main.js",
    vendor: "./src/vendor.js",
  },
  output: {
    filename: "[name].js",
    path: __dirname + "/dist",
  },
};

缺点

  • 如果入口文件之间共享了相同的模块,那么这些模块会被打包进每个入口的 bundle 中(造成重复代码打包)
  • 不够灵活,不能根据实际需要动态分离代码

方法二:防止重复

1.手动指定每个入口的 dependOn(只能共享无副作用的同步代码)

dependOn 允许你显式地声明当前入口文件依赖于其他入口 chunk。这样,Webpack 就不会把依赖的模块重复打包进当前的文件中,而是确保依赖的 chunk 先加载。

假设我们有两个入口 index 和 another,它们都共同使用了 lodash。我们可以把 lodash 单独提取为一个入口 shared,然后让另外两个入口依赖它。

const path = require("path");

module.exports = {
  mode: "development",
  entry: {
    // 1. 先定义一个共享的入口,只包含 lodash
    shared: ["lodash"],

    // 2. index 入口
    index: {
      import: "./src/index.js",
      // 关键点:告诉 webpack 这个 entry 依赖于 'shared' chunk
      dependOn: "shared",
      filename: "index.bundle.js",
    },

    // 3. another 入口
    another: {
      import: "./src/another-module.js",
      dependOn: "shared",
      filename: "another.bundle.js",
    },
  },
  output: {
    filename: "[name].bundle.js",
    path: path.resolve(__dirname, "dist"),
  },
};
  • 没有 dependOn 时: Webpack 会分析 index.js 发现它用了 lodash,于是把 lodash 打包进 index.bundle.js;同理,another.bundle.js 里也会有一份 lodash。这就造成了重复。
  • 使用 dependOn 后:
    • Webpack 会生成一个 shared.bundle.js,里面包含 lodash。
    • index.bundle.js 知道它需要 shared,所以它里面不会再有 lodash 的代码,只会包含加载 shared.bundle.js 的逻辑。
    • 当你在 HTML 中引用 index.bundle.js 时,Webpack 的运行时会确保 shared.bundle.js 被先加载。
2.自动代码分离- SplitChunksPlugin

SplitChunksPlugin 插件可以将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。让我们使用这个插件去除之前示例中重复的 lodash 模块:

module.exports = {
  // ...
  optimization: {
    splitChunks: {
      // 这里的配置项会覆盖默认配置,所以建议只覆盖需要修改的,或者在下面自定义 chunks

      chunks: "async", // (1) 作用范围:只对异步加载的模块生效 (all | initial | async)
      minSize: 30000, // (2) 最小尺寸:模块大于 30KB 才会被提取(压缩前)
      maxSize: 0, // (3) 最大尺寸:尝试将大于此值的 chunk 拆分成更小的部分(不保证一定成功)
      minChunks: 1, // (4) 最小引用次数:模块至少被引用 1 次才提取
      maxAsyncRequests: 5, // (5) 按需加载时的最大并行请求数
      maxInitialRequests: 3, // (6) 入口点的最大并行请求数
      automaticNameDelimiter: "~", // (7) 连接符:生成的文件名中间的符号 (如 vendors~main.js)
      name: true, // (8) 命名:true 表示自动生成文件名(基于 cacheGroups),也可以设为函数或字符串

      // (9) 缓存组:核心中的核心!SplitChunks 就是根据 cacheGroups 来拆分的
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 下的模块
          priority: -10, // 优先级:数字越大优先级越高
          name: "vendors", // 强制命名
        },
        default: {
          // 默认缓存组
          minChunks: 2, // 至少被 2 个 chunk 共享才提取
          priority: -20,
          reuseExistingChunk: true, // 如果当前 chunk 已经包含从主 bundle 中拆分出的模块,则重用该模块,而不是重新打包
        },
      },
    },
  },
};
关键配置详解

(1) chunks (作用范围) 这是最重要的配置之一,决定了哪些模块会被分析。

  • 'initial': 只处理入口文件的同步依赖。
  • 'async' (默认): 只处理异步加载的模块(即通过 import() 加载的)。这是为了不增加 HTTP 请求数量,如果只是小模块,不提取反而更好。
  • 'all': 同时处理同步和异步模块。推荐在生产环境使用 all,能最大程度利用缓存。

(2) minSize 与 maxSize

  • minSize: 如果一个拆分出来的包只有 1KB,提取它还需要发一次 HTTP 请求,得不偿失。所以默认 30KB 以下的模块不拆分。
  • maxSize: Webpack 会尝试将大包拆分成不超过 maxSize 的多个小包。这主要用于优化加载速度(大包加载慢,拆分后可以并行加载)。 (9) cacheGroups (缓存组)
  • 这是 SplitChunksPlugin 最强大的地方。Webpack 默认有两个组:vendors(第三方库)和 default(公共业务代码)。

匹配规则: 一个模块如果满足多个 cacheGroups 的条件,它会根据 priority(优先级)决定进入哪个组。 继承特性: 如果 cacheGroups 里没有设置 minSize、minChunks 等属性,它们会继承外层的配置。

方法三:动态导入

使用动态导入,webpack 会在构建阶段把动态导入的模块拆分成单独的 chunk,当代码真正执行到这行时,才会去加载这个 chunk

具体流程为

  1. 当代码执行到动态导入语句时,webpack 会分析这个模块的依赖关系
  2. 把这个模块及其所有依赖的模块,打包成一个单独的 chunk
  3. 生成一个对应的 JSONP 函数,用于异步加载这个 chunk
  4. 当代码真正执行到这行时,会调用 JSONP 函数,触发 chunk 的加载
  5. 加载完成后,执行这个 chunk 中的代码

例子

// main.js (主文件)

// 绑定一个按钮
document.getElementById("btn").addEventListener("click", () => {
  // 只有当用户点击按钮时,才会去加载 heavyMath.js
  import("./heavyMath.js").then((module) => {
    // 加载成功,执行里面的函数
    const doCalc = module.default;
    doCalc();
  });
});

此时 heavyMath.js 会被拆分成一个单独的 chunk,只有当用户点击按钮时,才会去加载这个 chunk

也就实现了代码分割