浅析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.jsindex.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,满足三个入口都引入other1other2,那么会打在一起吗?

很显然是打在一起的

如果在两个入口再引入other3.js满足other缓存组的条件呢?答案是和上面第一个问题一样的,产生两个bundleother1other2是一个,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,另外两个异步引入的other1other2也都分别打成了other-193-bundle.jsother-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

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

相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch7 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光7 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   7 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   7 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web7 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery