浅析webpack中的Code Spliting【代码分割】

认识Code Spliting(代码分割)

  • 默认情况下,一些第三方库以及一些首页用不到的业务代码很可能都打到相同的bundle中在第一时间加载,那将使首页加载的速度变慢
  • 代码分割的主要目的是将代码分割到不同的bundle中,这样就可以对bundle进行选择性的并行加载或者按需加载

webpack中Code Spliting的三种模式

  • 多入口对应多出口
  • 动态导入
  • 抽离代码 splitChunks 重点讲解

多入口对应多出口

先来看单入口

入口处为index.js

js 复制代码
    // index.js
    var index = 'index.js'
    console.log(index)

    var main = 'main.js'
    console.log(main)
js 复制代码
    // webpack
    ...
    entry: {
        index: getPath('src/code-spliting/index.js'),
    },
    output: {
        path: getPath('dist'),
        filename: 'js/[name].[chunkhash:8].bundle.js',
        clean: true
    },
    ...

毫无疑问,默认会输出一个bundle也就是index.js对应的内容。

如果想把上述输出内容一分为二,很简单,拆分多入口即可!

多入口

js 复制代码
    // main.js
    var main = 'main.js'
    console.log(main)
    
    // index.js
    var index = 'index.js'
    console.log(index)
js 复制代码
    // webpack
    ...
    entry: {
        index: getPath('src/code-spliting/index.js'),
        main: getPath('src/code-spliting/main.js'),
    },
    ...

看一下输出产物

动态导入

下面新建other1文件,并在index.js中动态引入

webpack内部对于动态导入的模块默认采用单独打包的形式

js 复制代码
...
	entry: {
		index: getPath('src/code-spliting/index.js'),
		main: getPath('src/code-spliting/main.js'),
	},
	output: {
		path: getPath('dist'),
		filename: 'js/[name].[chunkhash:8].bundle.js',
		chunkFilename: 'js/[name].bundle.js',
		clean: true
	},
...

dist输出产物:

就算在main.js中动态引入也是只会产生一个async-other.bundle.js

splitChunks

webpack官网的默认配置

yaml 复制代码
module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',
      minSize: 20000,
      minRemainingSize: 0,
      minChunks: 1,
      maxAsyncRequests: 30,
      maxInitialRequests: 30,
      enforceSizeThreshold: 50000,
      cacheGroups: {
        defaultVendors: {
          test: /[\/]node_modules[\/]/,
          priority: -10,
          reuseExistingChunk: true,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
      },
    },
  },
};

官网www.webpackjs.com/plugins/spl...

对后面能用到的几个关键的属性解释一下~

  • chunks :
    async (默认值) 处理异步 导入的代码
    initial 处理同步 导入的代码(注意:webpack默认支持异步导入代码的抽离不受影响)
    all 同步+异步代码都会进行处理
  • minSize:抽离出来的包最小的大小(抽离出来的包特别特别小,比给定的值还要小就没意义了,还要浪费一次http请求)
  • minChunks:被多入口引用的次数
  • cacheGroups :缓存组,某个模块在满足缓存组的规则之后不会立即抽离,而是等其他也满足该规则的模块打包到一组;(注意:每个缓存组内的属性优先级比外部splitChunks下的属性优先级高,外部的是统一的,内部的是针对当前缓存组来说的)
  • priority: 缓存组的权重,相同规则下权重高的命中
  • reuseExistingChunk:某个模块已经被抽离出来,直接服用不会再次打包

小试牛刀

  • 入口总共三个文件:entry.js,index.js,main.js
  • other文件夹下是要被引入的文件:other1.js,other2.js,other3.js
  • 每个文件内部仅仅只打印自身的文件名

initial模式

抽离公共模块

三个入口文件 分别引入other1.js import './other/other1.js'

下面看打包输出结果 三个入口对应输出的三个bundle,而且各自内部都嵌入一份other1的代码,并没有抽离代码。

这是因为默认情况下splitChunks.chunks为async,只对异步导入做抽离,我们的同步代码并没有抽离,下面尝试改一下配置。

js 复制代码
    // webpack
    optimization: {
        splitChunks: {
            chunks: 'initial',
            minSize: 0,
            minChunks: 3,  // 1;2;3 都可以,因为other1一共被引入了三次
            cacheGroups: {
                    defaultVendors: false,  // 取消默认缓存组
                    default: false // 取消默认缓存组
            }
        }
    },

输出结果不难看出other1被抽离出来了

抽离三方库

下载一个第三方库lodash在entry和index中引入,补充cacheGroups内部配置项

js 复制代码
    optimization: {
        splitChunks: {
            ...
            minChunks: 3, 
            cacheGroups: {
                defaultVendors: false,  // 取消默认缓存组
                default: false, // 取消默认缓存组,
                other: {
                        test: /other/,
                        filename: 'split/other-[id].bundle.js'
                },
                vendors: {
                        test: /node_modules/,
                        filename: 'split/vendors-[id].bundle.js'						
                }
            }
        }
    },

看下结果:other1被正常抽离到split目录下other-832.bundle.js

为啥lodash也是公共代码,但是没有被抽离呢? 是因为缓存组继承外层的minChunks为3,而lodash被引入了两次。我们把外层的minChunks(或者vendors缓存组的minChunks) 改成 2试一试。

果然不出所料!lodash也抽离成vendors-xxx.bundle.js了

再思考一下

这里补充一下上面说的把外层minChunks改成 2,其他没变

js 复制代码
    // webapck
    optimization: {
        splitChunks: {
            ...
            minChunks: 2, 
            cacheGroups: {
                defaultVendors: false,  // 取消默认缓存组
                default: false, // 取消默认缓存组,
                other: {
                        test: /other/,
                        filename: 'split/other-[id].bundle.js'
                },
                vendors: {
                        test: /node_modules/,
                        filename: 'split/vendors-[id].bundle.js'						
                }
            }
        }
    },

目前三个入口文件(entry,index,main)都引入了other1.js,我们在entry.js和index.js两个文件引入lodash的基础上再次同时新增引入other2.js ,显然other2已经满足other缓存组的条件了,思考一下other2会被打入到上面抽出的other1对应的other-832.bundle.js中吗?

看下输出结果 很显然除了other-832.bundle.js还有other-193.bundle.js,同一规则下两个相互独立的bundle

我的理解是这样的:对于三个入口文件来说other1是公共的 ,而对于entry和index来说other2是公共的,如果都打在一起的情况下对于entry和index是合理的,因为他们都用到了两个模块。但是对于第三个入口main是不合理的只引入了other1一个模块,所以是两个bundle。(在包的大小以及并发引用次数等一系列属性都理想化时)。

如果在main.js中也引入other2.js,满足三个入口都引入other1和other2,那么会打在一起吗?

很显然是打在一起的

如果在两个入口再引入other3.js满足other缓存组的条件呢?答案是和上面第一个问题一样的,产生两个bundle,other1和other2是一个,other3是一个。

也就是说理想情况下在满足同一缓存组条件的前提下,不同的模块被相同的文件引入可能会产生同一个包,被不同的文件引入产生不同的包

将引入的other打到一起

如果想要把满足缓存组条件的包都强制打到一起 ,就要设置一个name,如下代码会将满足other缓存组条件的代码全部打成一个other.bundle.js

js 复制代码
 // webpack
 cacheGroups: {
    ...
    other: {
        test: /other/,
        minChunks: 2,
        filename: 'split/other-[name].[id].bundle.js',
        name: 'all'
    },
    ...
 }

chunks设置initial情况下的异步代码

webpack本身就天然分割异步导入的代码,设置成initial只是对同步代码做出分割,并不会影响异步分割的代码(动态导入)

  • 三个入口文件entry.js , index.js , main.js 同步引入 other1.js;
    import './other/other1.js'
  • entry.js 异步引入 other2.js;
    import (/* webpackChunkName: "async-other2"*/ './other/other2.js')
  • index.js 同步引入 other2.js
    import './other/other2.js'

为了方便演示去掉other组内的name属性

js 复制代码
    ...
    output: {
        path: getPath('dist'),
        filename: 'js/[name].[chunkhash:8].bundle.js',
        chunkFilename: 'js/[name].bundle.js',
        clean: true
    },
    ...
    other: {
        test: /other/,
        minChunks: 2,
        filename: 'split/other-[id].bundle.js',
        // name: 'all'
    },
  • 对于other1来说,肯定是要抽出来的(熟悉的配方)
  • 对于异步引入的other2被独立抽离出来了
  • 对于同步引入的other2来说,只在index同步引入了一次,不满足抽离的条件被嵌入到index中了

async模式

采用如下依赖关系:

  • 三个入口文件entry.js , index.js , main.js 同步引入 other3.js;
    import './other/other3.js'
  • entry.js 异步引入 other1.js;
    import (/* webpackChunkName: "async-other1"*/ './other/other1.js')
  • index.js 异步引入 other2.js
    import (/* webpackChunkName: "async-other2"*/ './other/other2.js')

splitChunks.chunks默认值为async,我们把上述配置中的chunks改为async表示该组(other)只匹配异步代码

js 复制代码
    ...
    output: {
        path: getPath('dist'),
        filename: 'js/[name].[chunkhash:8].bundle.js',
        chunkFilename: 'js/[name].bundle.js',
        clean: true
    },
    ...
    optimization: {
        splitChunks: {
            ...
            ...
            other: {
                test: /other/,
                chunks: 'async'
                minChunks: 2,
                filename: 'split/other-[id].bundle.js',
            },
        }
    }

看下输出结果:

  • 同步代码并没有分割出来,这是意料之中的
  • 代码中采用异步引入的模块并没有根据缓存组的规则被抽离出来 ,依然采用默认的异步分割的方式,我们把minChunks改为 1试一下
js 复制代码
    // webpack
         ...
            other: {
                test: /other/,
                minChunks: 1,
                filename: 'split/other-[id].bundle.js',
            },
        ...

补充name属性,完善filename属性:

js 复制代码
    // webpack
         ...
            other: {
                test: /other/,
                minChunks: 1,
                filename: 'split/other-[name]-[id].bundle.js',
                name: 'all'
            },
        ...

果然,对于异步代码来说满足规则的部分会进行抽离,如果不满足则会天然分割成独立的包

all模式

chunks: all 不论是同步引入还是异步引入的代码都会匹配,把chunks改为'all'试一下

js 复制代码
    // webpack
         ...
        other: {
            test: /other/,
            chunks: 'all',
            minChunks: 1,
            filename: 'split/other-[id].bundle.js',
        },
        ...


other1 other2 other3 都符合other下的规则

同步引入的other3打成了other-682.bundle.js,另外两个异步引入的other1和other2也都分别打成了other-193-bundle.js和other-832-bundle.js

如果想把这一组打成一个包,那么就要设置name属性

js 复制代码
    // webpack
         ...
        other: {
            test: /other/,
            chunks: 'all',
            minChunks: 1,
            filename: 'split/other-[name]-[id].bundle.js',
            name: 'other-all'
        },
        ...


注意:all,对于同一模块即被同步引入又用异步引入,缓存组设置name会将包合并复用

总结一下

  • webpack本身就是天然对异步导入的代码进行独立分割的
  • 对于splitChunks.cacheGroups会把满足匹配条件的模块都按照该组的方式进行分割(可能打成多个包,也可能打成一个包),如果想把所有满足该组条件的模块全部打成一个包,要给该组设置一个固定的name(可以是一个字符串)
  • chunks: initial 只匹配同步导入代码,满足条件的的同步代码进行分割,不影响天然分割的异步代码块
  • chunks: async 只匹配异步导入代码,对满足条件的异步代码块进行分割,不满足的异步代码依然会被天然分割
  • chunks:all 匹配(同步+异步)导入代码,如果想把满足该组所有的chunk打成一个包,可以设置一个固定name

先写到这儿,路过的大佬有什么宝贵的建议,可以交流探讨一下~

相关推荐
lovingsoft4 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly14 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝18 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦19 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡39 分钟前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队1 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端