浅析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

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

相关推荐
霍先生的虚拟宇宙网络9 分钟前
webp 网页如何录屏?
开发语言·前端·javascript
jessezappy29 分钟前
jQuery-Word-Export 使用记录及完整修正文件下载 jquery.wordexport.js
前端·word·jquery·filesaver·word-export
旧林8431 小时前
第八章 利用CSS制作导航菜单
前端·css
yngsqq1 小时前
c#使用高版本8.0步骤
java·前端·c#
Myli_ing2 小时前
考研倒计时-配色+1
前端·javascript·考研
余道各努力,千里自同风2 小时前
前端 vue 如何区分开发环境
前端·javascript·vue.js
软件小伟2 小时前
Vue3+element-plus 实现中英文切换(Vue-i18n组件的使用)
前端·javascript·vue.js
醉の虾2 小时前
Vue3 使用v-for 渲染列表数据后更新
前端·javascript·vue.js
张小小大智慧2 小时前
TypeScript 的发展与基本语法
前端·javascript·typescript
hummhumm3 小时前
第 22 章 - Go语言 测试与基准测试
java·大数据·开发语言·前端·python·golang·log4j