webpack中的性能优化
构建性能
这里所说的构建性能,是指在开发阶段的构建性能,而不是生产环境的构建性能。
优化的目标,是降低从打包开始,到代码效果呈现所经过的时间。
减少模块解析
模块解析包括:抽象语法树分析、依赖分析、模块依赖函数替换。
如果没有模块解析打包过程就是这样的:
如果不解析某个模块,该模块经过loader处理后的代码就是最终代码,如果没有loader对该模块进行处理,该模块的源码就是最终打包结果的代码。
对于那些已经打包好了的第三方库,可以使用这种方式,缩短构建时间。例如:jquery
,它的 package.json
中main
字段指向的就是已经打包好的文件。
开启方式:配置 module.noParse
,被正则匹配到的模块不会解析。
优化loader性能
限制loader的应用范围
对于某些库,不使用loader,因为没有必要。例如:babel-loader
可以转换ES6或更高版本的语法,可是有些库本身就是用ES5语法书写的,不需要转换,使用babel-loader
反而会浪费构建时间。
通过 module.rule.exclude
或 module.rule.include
,排除或仅包含需要应用loader的场景
1 | module.exports = { |
这种做法是对loader的范围进行进一步的限制,和module.noParse
不冲突。
缓存loader的结果
如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变。可以将loader的解析结果保存下来,让后续的解析直接使用保存的结果。
loader的运行过程中,还包含一个过程,即pitch
,可以决定是否使用后序的loader。本质pitch
是loader的pitch
方法。
使用cache-loader
可以实现缓存的功能:
1 | module.exports = { |
为loader的运行开启多线程
运行loader的过程中会有大量的运算,如果项目很大loader很多运算量很大,可以使用多线程提高效率。
使用thread-loader
可以为后序的loader的运行开启多线程。
thread-loader
会开启一个线程池,线程池中包含适量的线程,它会把后续的loader放到线程池的线程中运行,以提高构建效率。
由于后续的loader会放到新的线程中,所以,后续的loader不能:
- 使用 webpack api 生成文件
- 无法使用自定义的 plugin api
- 无法访问 webpack options
所以在实际的开发中,需要进行测试,来决定thread-loader
放到什么位置比较合适。
传输性能
传输性能是指,打包后的JS代码传输到浏览器经过的时间。
在优化传输性能时要考虑到:
- 总传输量:所有需要传输的JS文件的内容加起来,就是总传输量,重复代码越少,总传输量越少
- 文件数量:访问页面时需要传输的JS文件数量,文件数量越多,http请求越多,响应速度越慢
- 浏览器缓存:JS文件会被浏览器缓存,被缓存的文件不会再进行传输
手动分包
基本原理
先单独的打包公共模块
公共模块会被打包成为动态链接库(Dynamic Link Library),并生成资源清单
根据入口模块进行正常打包
打包时,如果发现模块中使用了资源清单中描述的模块,则不会包含公共模块的代码
打包公共模块
打包公共模块是一个独立的打包过程,和主模块打包分开。
我们公共模块以jquery
和lodash
为例。
需要单独为公共模块配置一个配置文件:
1 | const webpack = require("webpack"); |
打包之后会形成这样的目录结构:
根目录的dll目录下是公共模块的资源清单:
1 | { |
使用公共模块
在页面模板中手动引入公共模块:
1 | <script src="./dll/jquery.js"></script> |
打包主模块
如果使用了插件clean-webpack-plugin
,为了避免它把公共模块清除,需要配置cleanOnceBeforeBuildPatterns
使用DllReferencePlugin
控制打包结果,需要用到刚才生成的资源清单:
1 | module.exports = { |
例如我们有两个chunk,其中都用到了公共模块:
1 | entry: { |
最后的打包结果就是这样的:
1 | { |
总结
手动打包的过程:
- 开启
output.library
暴露公共模块 - 用
DllPlugin
创建资源清单 - 用
DllReferencePlugin
使用资源清单
手动打包的注意事项:
- 资源清单不参与运行,可以不放到打包目录中
- 记得手动引入公共JS,以及避免被删除
- 不要对小型的公共JS库使用
优点:
- 极大提升自身模块的打包速度
- 极大的缩小了自身文件体积
- 有利于浏览器缓存第三方库的公共代码
缺点:
- 使用非常繁琐
- 如果第三方库中包含重复代码,则效果不太理想
自动分包
基本原理
不同与手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制
要控制自动分包,关键是要配置一个合理的分包策略
有了分包策略之后,不需要额外安装任何插件,webpack会自动的按照策略进行分包
- 分包策略至关重要,它决定了如何分包
- 分包时,webpack开启了一个新的chunk,对分离的模块进行打包
- 打包结果中,公共的部分被提取出来形成了一个单独的文件,它是新chunk的产物
分包策略的基本配置
webpack提供了optimization
配置项,用于配置一些优化信息,其中splitChunks
是分包策略的配置:
1 | module.exports = { |
分包策略有默认值:
chunks
该配置项用于配置需要应用分包策略的chunk
chunks
有三个取值,分别是:all
: 对于所有的chunk都要应用分包策略async
:【默认】仅针对异步chunk应用分包策略initial
:仅针对普通chunk应用分包策略
所以,只需要配置
chunks
为all
即可让每个chunk都分包automaticNameDelimiter
:新chunk名称的分隔符,默认值~minChunks
:一个模块被多少个chunk使用时,才会进行分包,默认值1minSize
:当包达到多少字节后才允许被真正的拆分,默认值30000maxSize
该配置可以控制包的最大字节数
如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包
但是分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的
代码压缩
目前最流行的代码压缩工具主要有两个:UglifyJs和Terser
webpack自动集成了Terser,当启用生产环境后自动用其进行代码压缩。
如果想更改、添加压缩工具,又或者是想对Terser进行配置,使用下面的webpack配置:
1 | const TerserPlugin = require('terser-webpack-plugin'); |
tree shaking
概念
压缩可以移除模块内部的无效代码,tree shaking 可以移除模块之间的无效代码。
例如像这样的代码:
myMath.js
1 | export function add(a, b){ |
index.js
1 | import {add} from "./myMath" |
对于myMath模块,我们只使用到了其中的add
函数,所以在打包结果中没有必要包含其他的函数。
tree shaking的作用就是用于移除掉那些不会用到的导出。
从webpack2开始,只要是生产环境,tree shaking
自动开启。
原理
webpack解析一个模块时会根据ES6的模块导入语句来判断,该模块依赖了另一个模块的哪个导出。
在具体分析依赖时,webpack坚持的原则是:保证代码正常运行,然后再尽量tree shaking。
依赖分析完毕后,webpack
会根据每个模块每个导出是否被使用,标记其他导出为dead code
,然后交给代码压缩工具处理。
代码压缩工具最终移除掉那些dead code
代码。
webpack之所以选择ES6的模块导入语句,是因为ES6模块的特点有利于分析出稳定的依赖:
- 导入导出语句只能是顶层语句
- import的模块名只能是字符串常量
- import绑定的变量是不可变的
在编写代码的时候尽量:
- 使用
export xxx
导出,而不使用export default {xxx}
导出 - 使用
import {xxx} from "xxx"
导入,而不使用import xxx from "xxx"
导入
所以对于使用commonjs模块方式的导出,tree shaking
是无法发挥作用的。
作用域分析
tree shaking
本身并没有完善的作用域分析,可能导致在一些dead code
函数中的依赖仍然会被视为依赖。
插件webpack-deep-scope-plugin
提供了作用域分析,可以解决这些问题。
副作用问题
webpack在tree shaking
的使用,有一个原则:一定要保证代码正确运行。
在满足该原则的基础上,再来决定如何tree shaking
。
因此,当webpack
无法确定某个模块是否有副作用时,它往往将其视为有副作用。
如果要解决该问题,就需要标记哪些文件是没有副作用的。
在package.json
中加入sideEffects
:
有两种配置方式:
false
:当前工程中,所有模块都没有副作用。- 数组:设置哪些文件拥有副作用,例如:
["!src/common.js"]
,表示只要不是src/common.js
的文件,都有副作用
1 | { |
这个通常是一些第三方库在它们自己的package.json
中标注。
css
webpack无法对css完成tree shaking,因此对css的tree shaking需要其他插件完成。
例如:purgecss-webpack-plugin
但是,purgecss-webpack-plugin
对css module
无能为力。
gzip
gzip是一种压缩文件的算法,在B/S结构中的压缩传输过程:
优点:传输效率可能得到大幅提升
缺点:服务器的压缩需要时间,客户端的解压需要时间
我们可以使用webpack进行预压缩,使用compression-webpack-plugin
插件对打包结果进行压缩,可以移除服务器的压缩时间: