webpack优化之代码分割与公共代码提取详解


Posted in Javascript onNovember 22, 2019

前言

开发多页应用的时候,如果不对webpack打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码)。当项目业务越来越复杂,打包出来的代码会非常冗余,文件体积会非常庞大。大体积文件会增加编译时间,影响开发效率;如果直接上线,还会拉长请求和加载时长,影响网站体验。作为一个追求极致体验的攻城狮,是不能忍的。所以在多页应用中优化打包尤为必要。那么如何优化webpack打包呢?

一、概念

在一切开始前,有必要先理清一下这三个概念:

  • module: 模块,在webpack眼里,任何可以被导入导出的文件都是一个模块。
  • chunk: chunk是webpack拆分出来的:
    • 每个入口文件都是一个chunk
    • 通过 import、require 引入的代码也是
    • 通过 splitChunks 拆分出来的代码也是
  • bundle: webpack打包出来的文件,也可以理解为就是对chunk编译压缩打包等处理后的产出。

二、问题分析

首先,简单分析下,我们刚才提到的打包问题:

  • 核心问题就是:多页应用打包后代码冗余,文件体积大。
  • 究其原因就是:相同模块在不同入口之间没有得到复用,bundle之间比较独立。

弄明白了问题的原因,那么大致的解决思路也就出来了:

  • 我们在打包的时候,应该把不同入口之间,共同引用的模块,抽离出来,放到一个公共模块中。这样不管这个模块被多少个入口引用,都只会在最终打包结果中出现一次。——解决代码冗余。
  • 另外,当我们把这些共同引用的模块都堆在一个模块中,这个文件可能异常巨大,也是不利于网络请求和页面加载的。所以我们需要把这个公共模块再按照一定规则进一步拆分成几个模块文件。——减小文件体积。
  • 至于如何拆分,方式因人而异,因项目而异。我个人的拆分原则是:
    • 业务代码和第三方库分离打包,实现代码分割;
    • 业务代码中的公共业务模块提取打包到一个模块;
    • 第三方库最好也不要全部打包到一个文件中,因为第三方库加起来通常会很大,我会把一些特别大的库分别独立打包,剩下的加起来如果还很大,就把它按照一定大小切割成若干模块。

optimization.splitChunks

webpack提供了一个非常好的内置插件帮我们实现这一需求:CommonsChunkPlugin。不过在 webpack4 中CommonsChunkPlugin被删除,取而代之的是optimization.splitChunks, 所幸的是optimization.splitChunks更强大!

三、 实现

通过一个多页应用的小demo,我们一步一步来实现上述思路的配置。

demo目录结构:

|--public/
|   |--a.html
|   |--index.html
|--src/
|   |--a.js
|   |--b.js
|   |--c.js
|   |--index.js
|--package.json
|--webpack.config.js

代码逻辑很简单,index模块中引用了 a 和 b 2个模块,a 模块中引用了 c 模块和 jquery库,b 模块中也引用了 c 模块和 jquery 库,c 是一个独立的模块没有其他依赖。

index.js代码如下:

//index.js
import a from './a.js';
import b from './b.js';
function fn() {
 console.log('index-------');
}
fn();

a.js代码如下:

//a.js
require('./c.js');
const $ = require('jquery')
function fn() {
 console.log('a-------');
}
module.exports = fn();

b.js代码如下:

//b.js
require('./c.js');
const $ = require('jquery')
function fn() {
 console.log('b-------');
}
module.exports = fn();

c.js代码如下:

//c.js
function fn() {
 console.log('c-------');
}
module.exports = fn();

1. 基本配置

webpack先不做优化,只做基本配置,看看效果。项目配置了2个入口,搭配html-webpack-plugin实现多页打包:

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
 entry: {
 index: './src/index.js',
 a: './src/a.js'
 },
 output: {
 path: path.resolve(__dirname, 'dist'),
 filename: '[name].js'
 },
 plugins: [
 new HtmlWebpackPlugin({
  template: './public/index.html',
  filename: 'index.html'
 }),
 new HtmlWebpackPlugin({
  template: './public/a.html',
  filename: 'a.html'
 })
 ]
}

在开发模式下运行webpack:

webpack优化之代码分割与公共代码提取详解

可以看到,打包出两个html和两个体积很大的(300多K)的文件a.js,index.js。

进入dist目录检查js文件:

  • a.js里包含c模块代码和jquery代码
  • index.js里包含a模块、b模块、c模块和jquery代码

看,同样的代码c和jquery被打包了2遍。

2. 初步添加splitChunks优化配置

首先解决相同代码打包2次的问题,我们需要让webpack把c和jquery提取出来打包为公共模块。

在webpack配置文件添加splitChunks:

//webpack.config.js

optimization: {
 splitChunks: {
 cacheGroups: {
  default: {
  name: 'common',
  chunks: 'initial'
  }
 }
 }
}

- cacheGroups

  • cacheGroups是splitChunks配置的核心,对代码的拆分规则全在cacheGroups缓存组里配置。
  • 缓存组的每一个属性都是一个配置规则,我这里给他的default属性进行了配置,属性名可以不叫default可以自己定。
  • 属性的值是一个对象,里面放的我们对一个代码拆分规则的描述。

- name

  • name:提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是index~a.js这样的。

- chunks

  • chunks:指定哪些类型的chunk参与拆分,值可以是string可以是函数。如果是string,可以是这个三个值之一:all, async, initial,all 代表所有模块,async代表只管异步加载的, initial代表初始化时就能获取的模块。如果是函数,则可以根据chunk参数的name等属性进行更细致的筛选。

再次打包:

webpack优化之代码分割与公共代码提取详解

可以看到a.js,index.js从300多K减少到6点几K。同时增加了一个common.js文件,并且两个打包入口都自动添加了common.js这个公共模块:

webpack优化之代码分割与公共代码提取详解

进入dist目录,依次查看这3个js文件:

  • a.js里不包含任何模块的代码了,只有webpack生成的默认代码。
  • index.js里同样不包含任何模块的代码了,只有webpack生成的默认代码。
  • common.js里有a,b,c,index,jquery代码。

发现,提是提取了,但是似乎跟我们预料的不太一样,所有的模块都跑到common.js里去了。

这是因为我们没有告诉webpack(splitChunks)什么样的代码为公共代码,splitChunks默认任何模块都会被提取。

- minChunks

splitChunks是自带默认配置的,而缓存组默认会继承这些配置,其中有个minChunks属性:

  • 它控制的是每个模块什么时候被抽离出去:当模块被不同entry引用的次数大于等于这个配置值时,才会被抽离出去。
  • 它的默认值是1。也就是任何模块都会被抽离出去(入口模块其实也会被webpack引入一次)。

我们上面没有配置minChunks,只配置了name和chunk两个属性,所以minChunks的默认值 1 生效。也难怪所有的模块都被抽离到common.js中了。

优化一下,在缓存组里配置minChunks覆盖默认值:

//webpack.config.js

optimization: {
 splitChunks: {
  cacheGroups: {
   default: {
    name: 'common',
    chunks: 'initial',
    minChunks: 2 //模块被引用2次以上的才抽离
   }
  }
 }
}

然后运行webpack

webpack优化之代码分割与公共代码提取详解

可以看到有2个文件的大小发生了变化:common.js由314K减小到311K,index.js由6.22K增大到7.56K。

进入dist目录查看:

a.js里依然不包含任何模块的代码(正常,因为a作为模块被index引入了一次,又作为入口被webpack引入了一次,所以a是有2次引用的)。

  • index.js里出现了b和index模块的代码了。
  • common.js里只剩a,c,和jquery模块的代码。
  • 现在我们把共同引用的模块a, c, jquery,从a和index这两个入口模块里抽取到common.js里了。有点符合我们的预期了。

3. 配置多个拆分规则

3.1 实现代码分离,拆分第三方库

接下来,我希望公共模块common.js中,业务代码和第三方模块jquery能够剥离开来。

我们需要再添加一个拆分规则。

//webpack.config.js

optimization: {
 splitChunks: {
  minSize: 30, //提取出的chunk的最小大小
  cacheGroups: {
   default: {
    name: 'common',
    chunks: 'initial',
    minChunks: 2, //模块被引用2次以上的才抽离
    priority: -20
   },
   vendors: { //拆分第三方库(通过npm|yarn安装的库)
    test: /[\\/]node_modules[\\/]/,
    name: 'vendor',
    chunks: 'initial',
    priority: -10
   }
  }
 }
}

我给cacheGroups添加了一个vendors属性(属性名可以自己取,只要不跟缓存组下其他定义过的属性同名就行,否则后面的拆分规则会把前面的配置覆盖掉)。

- minSize

minSize设置的是生成文件的最小大小,单位是字节。如果一个模块符合之前所说的拆分规则,但是如果提取出来最后生成文件大小比minSize要小,那它仍然不会被提取出来。这个属性可以在每个缓存组属性中设置,也可以在splitChunks属性中设置,这样在每个缓存组都会继承这个配置。这里由于我的demo中文件非常小,为了演示效果,我把minSize设置为30字节,好让公共模块可以被提取出来,正常项目中不用设这么小。

- priority

priority属性的值为数字,可以为负数。作用是当缓存组中设置有多个拆分规则,而某个模块同时符合好几个规则的时候,则需要通过优先级属性priority来决定使用哪个拆分规则。优先级高者执行。我这里给业务代码组设置的优先级为-20,给第三方库组设置的优先级为-10,这样当一个第三方库被引用超过2次的时候,就不会打包到业务模块里了。

- test

test属性用于进一步控制缓存组选择的模块,与chunks属性的作用有一点像,但是维度不一样。test的值可以是一个正则表达式,也可以是一个函数。它可以匹配模块的绝对资源路径或chunk名称,匹配chunk名称时,将选择chunk中的所有模块。我这里用了一个正则/[\\/]node_modules[\\/]/来匹配第三方模块的绝对路径,因为通过npm或者yarn安装的模块,都会存放在node_modules目录下。

运行一下webpack:

webpack优化之代码分割与公共代码提取详解

可以看到新产生了一个叫vendor.js的文件(name属性的值),同时common.js文件体积由原来的311k减少到了861bytes!

进入dist目录,检查js文件:

  • a.js里不包含任何模块代码。
  • common.js只包含a和c模块的代码。
  • index.js只包含b和index模块的代码。
  • vendor.js只包含jquery模块的代码。

现在,我们在上一步的基础上,成功从common.js里把第三方库jquery抽离出来放到了vendor.js里。

3.2 拆分指定文件

如果我们还想把项目中的某一些文件单独拎出来打包(比如工程本地开发的组件库),可以继续添加拆分规则。比如我的src下有个locallib.js文件要单独打包,假设a.js中引入了它。

//a.js
require('./c.js');
require('./locallib.js'); //引入自己本地的库
const $ = require('jquery')
function fn() {
 console.log('a-------');
}
module.exports = fn();

可以这么配置:

//webpack.config.js

optimization: {
 splitChunks: {
  minSize: 30, //提取出的chunk的最小大小
  cacheGroups: {
   default: {
    name: 'common',
    chunks: 'initial',
    minChunks: 2, //模块被引用2次以上的才抽离
    priority: -20
   },
   vendors: { //拆分第三方库(通过npm|yarn安装的库)
    test: /[\\/]node_modules[\\/]/,
    name: 'vendor',
    chunks: 'initial',
    priority: -10
   },
   locallib: { //拆分指定文件
    test: /(src\/locallib\.js)$/,
    name: 'locallib',
    chunks: 'initial',
    priority: -9
   }
  }
 }
}

我在缓存组下又新增了一个拆分规则,通过test正则指定我就要单独打包src/locallib.js文件,并且把优先级设置为-9,这样当它被多次引用时,不会进入其他拆分规则组,因为另外两个规则的优先级都比它要低。

运行webpack打包后:

webpack优化之代码分割与公共代码提取详解

可以看到新产生了一个locallib.js文件。进入dist目录查看:

  • a.js里不包含任何模块代码。
  • common.js只包含a和c模块的代码。
  • index.js只包含b和index模块的代码。
  • vendor.js只包含jquery模块的代码。
  • locallib.js里只包含locallib模块的代码。

现在我们又在上一步的基础上独立打包了一个指定的模块locallib.js。

至此,我们就成功实现了抽离公共模块、业务代码和第三方代码剥离、独立打包指定模块。

对比一下,优化前,打包出来js一共有633KB:

webpack优化之代码分割与公共代码提取详解

优化后,打包出来js一共不到330KB:

webpack优化之代码分割与公共代码提取详解

优化打包后的文件分类清晰,体积比优化前缩小了几乎50%,有点小完美是不是!击掌!这还只是我举的一个简单例子,在实际多页应用中,优化力度说不定还不止这么多。

小结

webpack很强大,以上只是冰山一角,但是只要掌握了上述optimization.splitChunks的核心配置,我们就可以几乎随心所欲地按照自己的想法来拆分优化代码控制打包文件了,是不是很酷?玩转代码拆分,你也可以!

如果觉得这些依然不能满足你的需求,还想更精(bian)细(tai)地定制打包规则,可以到webpack官网查看optimization.splitChunks的更多配置。

欢迎交流~

本文的完整webpack配置和demo源码可以在这里获取:https://github.com/yc111/webpack-optimize-demo

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
背景音乐每次刷新都可以自动更换
Feb 01 Javascript
jQuery EasyUI API 中文文档 - Spinner微调器使用
Oct 21 Javascript
datagrid框架的删除添加与修改
Apr 08 Javascript
jQuery实现Tab菜单滚动切换的方法
Sep 21 Javascript
Bootstrap 设置datetimepicker在屏幕上面弹出设置方法
Mar 21 Javascript
Vue中使用的EventBus有生命周期
Jul 12 Javascript
从零开始学习搭建React脚手架项目
Aug 23 Javascript
使用Object.defineProperty如何巧妙找到修改某个变量的准确代码位置
Nov 02 Javascript
用Cordova打包Vue项目的方法步骤
Feb 02 Javascript
详解vuex持久化插件解决浏览器刷新数据消失问题
Apr 15 Javascript
11个教程中不常被提及的JavaScript小技巧(推荐)
Apr 17 Javascript
js 判断当前时间是否处于某个一个时间段内
Sep 19 Javascript
小程序实现录音上传功能
Nov 22 #Javascript
vue使用recorder.js实现录音功能
Nov 22 #Javascript
微信小程序开发摇一摇功能
Nov 22 #Javascript
js实现录音上传功能
Nov 22 #Javascript
解决vue自定义全局消息框组件问题
Nov 22 #Javascript
JavaScript实现省市联动效果
Nov 22 #Javascript
Vue混入mixins滚动触底的方法
Nov 22 #Javascript
You might like
php5中date()得出的时间为什么不是当前时间的解决方法
2008/06/30 PHP
编写php应用程序实现摘要式身份验证的方法详解
2013/06/08 PHP
php使用crypt()函数进行加密
2017/06/08 PHP
ThinkPHP框架实现的MySQL数据库备份功能示例
2018/05/24 PHP
jQuery性能优化技巧分析
2015/02/20 Javascript
jQuery 1.9.1源码分析系列(十四)之常用jQuery工具
2015/12/02 Javascript
jQuery formValidator表单验证
2016/01/07 Javascript
实例详解ECMAScript5中新增的Array方法
2016/04/05 Javascript
关于JS 预解释的相关理解
2016/06/28 Javascript
HTML5 实现的一个俄罗斯方块实例代码
2016/09/19 Javascript
Bootstrap 模态框实例插件案例分析
2016/12/28 Javascript
基于JavaScript实现带缩略图的轮播效果
2017/01/12 Javascript
jquery 实时监听输入框值变化的完美方法(必看)
2017/01/26 Javascript
原生JS实现Ajax跨域请求flask响应内容
2017/10/24 Javascript
AngularJS与后端php的数据交互方法
2018/08/13 Javascript
JS中使用new Option()实现时间联动效果
2018/12/10 Javascript
vue实现学生信息管理系统
2020/05/30 Javascript
[01:33:14]LGD vs VP Supermajor 败者组决赛 BO3 第二场 6.10
2018/07/04 DOTA
pymssql ntext字段调用问题解决方法
2008/12/17 Python
python实现发送邮件功能
2017/07/22 Python
Python单例模式的两种实现方法
2017/08/14 Python
python tkinter组件摆放方式详解
2019/09/16 Python
AUC计算方法与Python实现代码
2020/02/28 Python
Python如何使用vars返回对象的属性列表
2020/10/17 Python
瑞典快乐袜子:Happy Socks
2018/02/16 全球购物
监理员的岗位职责
2013/11/13 职场文书
创先争优活动方案
2014/02/12 职场文书
幼儿园优秀教师事迹
2014/02/13 职场文书
2014全国两会学习心得体会1000字
2014/03/10 职场文书
美国留学经济担保书
2014/05/20 职场文书
离婚协议书范本及离婚须知
2014/10/15 职场文书
小学生作文之《压岁钱的烦恼》
2019/09/27 职场文书
css3 实现文字闪烁效果的三种方式示例代码
2021/04/25 HTML / CSS
ORACLE查看当前账号的相关信息
2021/06/18 Oracle
详解pytorch创建tensor函数
2022/03/22 Python
Redis实战高并发之扣减库存项目
2022/04/14 Redis