Webpack配置全解析(优化篇)
在上一篇文章Webpack配置全解析介绍了Webpack中loader和plugins的一些基本用法,当loader和plugins使用较多后项目也会越来越耗时,因此这次我们继续学习如何优化webpack的配置来让我们的项目运行的更快耗时更短。
本文将从缩小文件搜索范围、减少打包文件、缓存和多进程四个方面来了解Webpack的优化配置。
缩小文件搜索范围
Webpack会从Entry入口出发,解析文件中的导入模块语句,再递归解析;每次遇到导入语法时会做两件事情:
- 查找导入模块的位置,比如
require('vue')
就去引入/node_modules/vue/dist/vue.runtime.common.js
文件 - 通过相应的loader来解析导入的模块,比如引入的js就调用babel-loader来转换代码
当项目只有几个文件时,解析文件流程只有几百毫秒,然而随着项目规模的增大,解析文件会越来越耗时,因此我们通过webpack的配置来缩小我们搜索模块的范围
优化loader配置
在上一篇中,我们介绍了使用include/exclude
将node_modules中的文件进行包括/排除。
1 |
|
include
表示哪些目录中的文件需要进行babel-loader,exclude
表示哪些目录中的文件不要进行babel-loader。这是因为在引入第三方模块的时候,很多模块已经是打包后的,不需要再被处理,比如vue、jQuery等;如果不设置include/exclude
就会被loader处理,增加打包时间。
优化module.noParse配置
如果一些第三方模块没有使用AMD/CommonJs规范,可以使用noParse
来标记这个模块,这样Webpack在导入模块时,就不进行解析和转换,提升Webpack的构建速度;noParse可以接受一个正则表达式或者一个函数:
1 |
|
对于jQuery、lodash、chartjs等一些库,庞大且没有采用模块化标准,因此我们可以选择不解析他们。
注:被不解析的模块文件中不应该包含
require
、import
等模块语句
经过多次打包尝试,打包性能大概能提升10%~20%;本实例完整代码demo,
优化resolve.modules配置
modules用于告诉webpack去哪些目录下查找引用的模块,默认值是["node_modules"]
,意思是在./node_modules
查找模块,找不到再去../node_modules
,以此类推。
我们代码中也会有大量的模块被其他模块依赖和引入,由于这些模块位置分布不固定,路径有时候会很长,比如import '../../src/components/button'
、import '../../src/utils'
;这时我们可以利用modules进行优化
1 |
|
这样我们可以简单的通过import 'components/button'
、import 'utils'
进行导入,webpack会会优先从src
目录下进行查找
优化resolve.alias配置
alias通过创建import或者require的别名,把原来导入模块的路径映射成一个新的导入路径;它和resolve.modules
不同的的是,它的作用是用别名代替前面的路径,不是省略;这样的好处就是webpack直接会去对应别名的目录查找模块,减少了搜索时间。
1 |
|
这样我们就能通过import Buttom from '@/Button'
来引入组件了;我们不光可以给自己写的模块设置别名,还可以给第三方模块设置别名:
1 |
|
我们在import Vue from 'vue'
时,webpack就会帮我们去vue依赖包的dist文件下面引入对应的文件,减少了搜索package.json的时间。
优化resolve.mainFields配置
mainFields用来告诉webpack使用第三方模块中的哪个字段来导入模块;第三方模块中都会有一个package.json
文件用来描述这个模块的一些属性,比如模块名(name)、版本号(version)、作者(auth)等等;其中最重要的就是有多个特殊的字段用来告诉webpack导入文件的位置,有多个字段的原因是因为有些模块可以同时用于多个环境,而每个环境可以使用不同的文件。
mainFields的默认值和当前webpack配置的target
属性有关:
- 如果target为
webworker
或web
(默认),mainFields默认值为["browser", "module", "main"]
- 如果target为其他(包括node),mainFields默认值为
["module", "main"]
这就是说当我们require('vue')
的时候,webpack先去vue下面搜索browser字段,没有找到再去搜索module字段,最后搜索main字段。
为了减少搜索的步骤,在明确第三方模块入口文件描述字段时,我们可以将这个字段设置尽量少;一般第三方模块都采用main
字段,因此我们可以这样配置:
1 |
|
优化resolve.extensions配置
extensions字段用来在导入模块时,自动带入后缀尝试去匹配对应的文件,它的默认值是:
1 |
|
也就是说我们在require('./utils')
时,Webpack先匹配utils.js
,匹配不到再去匹配utils.json
,如果还找不到就报错。
因此extensions
数组越长,或者正确后缀的文件越靠后,匹配的次数越多也就越耗时,因此我们可以从以下几点来优化:
- extensions数组尽量少,项目中不存在的文件后缀不要列进去
- 出现频率比较高的文件后缀优先放到最前面
- 在代码中导入文件的时候,要尽量把后缀名带上,避免查找
以上实例完整代码demo。
减少打包文件
在我们项目中不可避免会引入第三方模块,webpack打包时也会将第三方模块作为依赖打包进bundle中,这样就会增加打包文件尺寸和增加耗时,如果能合理得处理这些模块就能提升不少webpack的性能。
提取公共代码
我们的项目通常有多个页面或者多个页面模块(单页面),多个页面之间通常都有公用的函数或者第三方模块,在每个页面中都打包这些模块会造成以下问题:
- 资源重复加载,浪费用户流量
- 每个页面加载资源多,首屏展示慢
在Webpack4之前,都是通过CommonsChunkPlugin插件来提取公共代码,然而存在着以下问题
- 产出的chunk在引入的时候,会包含重复的代码
- 无法优化异步chunk
Webpack4引入了SplitChunksPlugin
插件进行公共模块的抽取;由于webpack4开箱即用的特性,它不用单独安装,通过optimization.splitChunks
进行配置即可,官方给的默认配置参数如下:
1 |
|
我们在home、list、detail三个页面分别引入了vue.js、axios.js和公用的工具函数模块utils.js;我们首先将使用到的第三方模块提取到一个单独的文件,这个文件包含了项目的基础运行环境,一般称为vendors.js
;在抽离第三方模块后我们将每个页面都依赖的公共代码提取出来,放到common.js
中。
1 |
|
有时候项目依赖模块比较多,vendors.js
文件会特别大,我们还可以对它进一步拆分,按照模块划分:
1 |
|
动态链接DllPlugin
DLL即动态链接库(Dynamic-Link Library)的缩写,熟悉Windows系统的童鞋在电脑中也经常能看到后缀是dll的文件,偶尔电脑弹框警告也是因为电脑中缺失了某些dll文件;DLL最初用于节约应用程序所需的磁盘和内存空间,当多个程序使用同一个函数库时,DLL可以减少在磁盘和内存中加载代码的重复量,有助于代码的复用。
在Webpack中也引入了DLL的思想,把我们用到的模块抽离出来,打包到单独的动态链接库中去,一个动态链接库中可以有多个模块;当我们在多个页面中用到某一个模块时,不再重复打包,而是直接去引入动态链接库中的模块。
Webpack中集成了对动态链接库的支持,主要用到的两个插件:
- DllPlugin:创建动态链接库文件
- DllReferencePlugin:在主配置中引入打包好的动态链接库文件
我们首先使用DllPlugin来创建动态链接库文件,在项目下新建webpack.dll.js
文件:
1 |
|
这里entry
设置了多个入口,每个入口也有多个模块文件;然后在package.json
添加打包命令
1 |
|
执行npm run build:dll
后,我们在/public/vendor
目录下得到了我们打包后的动态链接库的文件:
1 |
|
生成出来的打包文件正好是以两个入口名来命名的,以vue为例,看一下vue.dll.js
的内容:
1 |
|
可以看出,动态链接库中包含了引入模块的所有代码,这些代码存在一个对象中,通过模块路径作为键名来进行引用;并且通过vue_dll_lib暴露到全局;vue.manifest.json则是用来描述动态链接库文件中包含了哪些模块:
1 |
|
manifest.json描述了对应js文件包含哪些模块,以及对应模块的键名(id),这样我们在模板页面中就可以将动态链接库作为外链引入,当Webpack解析到对应模块时就通过全局变量来获取模块:
1 |
|
最后我们在打包时,通过DllReferencePlugin
将动态链接库引入到主配置中:
1 |
|
注:动态链接库打包到
/public/vendor
目录下,还需要通过CopyWebpackPlugin
插件将它拷贝到生成后的目录中,否则会出现引用失败的报错;打包动态链接库文件只需要执行一次,除非以后模块升级或者引入新的模块。
引入动态链接库可以将项目中一些不经常更新的模块放到外部文件中,我们再次打包页面逻辑代码时会发现构建速度有了比较大的提升,大概30%~40%,相关代码在demo10。
externals
我们在项目打包时,有一些第三方的库会从CDN引入(比如jQuery等),如果在bundle中再次打包项目就过于臃肿,我们就可以通过配置externals
将这些库在打包的时候排除在外。
1 |
|
这样就表示当我们遇到require('jquery')
时,从全局变量去引用jQuery
,其他几个包也同理;这样打包时就把jquery、react、vue和react-dom从bundle中剔除了,本实例完整代码demo。
Tree Shaking
Tree Shaking最早由rollup实现,后来webpack2页实现了这项功能;Tree Shaking的字面意思是摇树,一棵树上有一些树叶虽然还挂着,但是它可能已经死掉了,通过摇树方式把这些死掉的树叶去除。
我们项目中也是同样的,我们并没有用到文件的所有模块,但是webpack仍会将整个文件打包进来,文件中一直用不到的代码就是“死代码”;这种情况就用用到Tree Shaking
帮我们剔除这些用不到的代码模块。
比如我们定义了一个utils.js
文件导出了很多工具模块,然后在index.js
中只引用了某些模块:
1 |
|
我们希望在代码中只打包isArray
函数到bundle中;需要注意的是,为了让Tree Shaking生效,我们需要使用ES6模块化的语法,因为ES6模块语法是静态化加载模块,它有以下特点:
- 静态加载模块,效率比CommonJS 模块的加载方式高
- ES6 模块是编译时加载,使得静态分析成为可能进一步拓宽JS的语法
如果是require
,在运行时确定模块,那么将无法去分析模块是否可用,只有在编译时分析,才不会影响运行时的状态。
使用ES6模块后还有一个问题,因为我们的代码一般都采用babel进行编译,而babel的preset默认会将任何模块类型编译成Commonjs,因此我们还需要修改.babelrc
配置文件:
1 |
|
配置好babel后我们需要让webpack先将“死代码”标识出来:
1 |
|
运行打包命令后,当我们打开输出的bundle文件时,我们发现虽然一些“死代码”还存在里面,但是加上了一个unused harmony export
的标识
1 |
|
虽然webpack给我们指出了哪些函数用不上,但是还需要我们通过插件来剔除;由于uglifyjs-webpack-plugin
不支持ES6语法,这里我们使用terser-webpack-plugin
的插件来代替它:
1 |
|
这样我们发现打包出来的文件就没有多余的代码了。
注: Tree Shaking在生产环境(production)是默认开启的
对于我们常用的一些第三方模块,我们也可以实现Tree Shaking;以lodash
为例,它整个包有非常多的函数,但并不是所有的函数都是我们所用到的,因此我们也需要对它没有用到的代码进行剔除。
1 |
|
打包出来发现包的大小还是能达到70+kb,如果只引用了chunk不应该有这么大;我们打开/node_modules/lodash/index.js
发现他还是使用了require的模式导入导出模块,因此导致Tree Shaking失败;我们先安装使用ES6模块版本的lodash:npm i -S lodash-es
,然后修改引入包:
1 |
|
这样我们生成的bundle包就小很多;本实例完整代码demo。
缓存
我们知道webpack会对不同的文件调用不同的loader进行解析处理,解析的过程也是最耗性能的过程;我们每次改代码也只是修改项目中的少数文件,项目中的大部分文件改动的次数不是那么频繁;那么如果我们将解析文件的结果缓存下来,下次发现同样的文件只需要读取缓存就能极大的提升解析的性能。
cache-loader
cache-loader可以将一些对性能消耗比较大的loader生产的结果缓存在磁盘中,等下次再次打包时如果是相同的代码就可以直接读取缓存,减少性能消耗。
注:保存和读取缓存也会产生额外的性能开销,因此cache-loader适合用于对性能消耗较大的loader,否则反而会增加性能消耗
cache-loader的使用也非常简单,安装后在所需要缓存的loader前面添加即可(因为loader加载的顺序是反向的),比如我们需要给babel-loader
添加缓存:
1 |
|
然而我们发现第一次打包的速度并没有发生明显变化,甚至可能还比原来打包的更慢了;同时还多了/node_modules/.cache/cache-loader/
这个目录,看名字就是一个缓存文件;我们继续打包,下面图表记录了我几次打包的耗时:
我们发现第一次打包时间都差不多,但是第二次开始缓存文件就开始发挥了重要的作用了,直接减少了75%的耗时。
除了使用cache-loader,babel-loader也提供缓存功能,通过cacheDirectory
进行配置:
1 |
|
在/node_modules/.cache/babel-loader
也多了缓存文件。经过两个使用结果的对比,cache-loader的性能提升更加出色一些;本实例完整代码demo。
HardSourceWebpackPlugin
HardSourceWebpackPlugin也可以为模块提供缓存功能,同意也是将文件缓存在磁盘中
首先通过npm i -D hard-source-webpack-plugin
来安装插件,并且在配置中添加插件:
1 |
|
一般HardSourceWebpackPlugin默认缓存是在/node_modules/.cache/hard-source/[hash]
目录下,我们可以设置它的缓存目录和何时创建新的缓存哈希值。
1 |
|
通过尝试多次打包,发现能节省大概90%的时间;本实例完整代码demo。
多进程
我们在事件循环中讲到过,js是一门单线程的语言,在同一事件线上只有一个线程在处理任务;因此在webpack解析到JS、CSS、图片或者字体文件时,它需要一个个的去解析编译,不能同时处理多个任务;我们可以通过插件来将任务分给多个子进程去并发执行,子进程处理完成后再将结果发送给主进程。
happypack
happypack会自动帮我们分解任务和管理进程,通过名字我们也能看出来,这是一款能够带来快乐的插件。
我们通过npm i -D happypack
后就能在webpack中进行配置了:
1 |
|
我们将rules/loader
的处理全部交给了happypack进行处理,并且通过id来调用具体的实例,然后在实例中配置具体的loader进行处理;在happypack的实例中除了id和loaders我们还可以配置进程数量:
1 |
|
注:threads和threadPool字段只需要配置一个即可。
我们通过happypack.ThreadPool
创建了一个包含5个子进程的共享进程池,每个happypack实例可以通过共享进程池来处理文件;相对于给每个happypack实例分配进程,这样可以防止占用过多无用的进程;我们打包看一下所耗时间:
我们发现有了happypack耗时居然还增加了20%~30%,说好的多进程带来快乐呢。
由于我们的项目不够庞大,而加载多进程也需要耗费时间和性能,因此我们才会出现使用了happypack反而增加耗时的情况;所以一般happypack适用于比较大的项目中;本实例完整代码demo。
thread-loader
把thread-loader放置在其他loader之前,在它之后的loader就会在一个单独的进程池中运行,但是在进程池中运行的loader有以下限制:
- 这些 loader 不能产生新的文件。
- 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
- 这些 loader 无法获取 webpack 的选项设置。
因此,也就是说像MiniCssExtractPlugin.loader
等一些提取css的loader是不能使用thread-loader的;跟happypack一样,它也只适合用于文件较多的大项目:
1 |
|
本实例完整代码demo。
本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。