本文内容来自三十分钟掌握Webpack性能优化
什么是webpack
它是一个模块打包器,它分析各个模块的依赖关系,最终打包成我们常见的静态文件,.js、.css、.jpg、.png等文件。
webpack相关知识点
-
HMR
-
resolve
-
optimization
-
dll
-
webpack5
-
production
-
development
-
eslint
-
babel
-
pwa
-
loader
-
plugin
-
devtool
-
性能优化
-
tree shaking
-
code split
-
caching
-
lazy loading
-
library
-
shimming
webpack基本原理
第一步:
-
webpack分析依赖是从一个入口文件 entry 开始分析。
-
从entry出发,webpack就会通过这个文件的路径读取entry文件的信息(读取到的本质其实是字符串),包含entry文件和它所依赖的所有依赖dependencies模块。
-
webpack会把读取到entry相关的信息转成AST(抽象语法树)。
第二步:
-
通过广度优先遍历dependencies数组,全部转成AST,同时通过mapping对应依赖项和该项的id。
-
最后生成一个依赖图谱,所有模块的依赖分别完成。
第三步:
- 现在已经能拿到依赖图谱,我们再通过调用bundle函数,其实bundle函数就是返回我们构造的字符串,拿到字符串,我们把字符串导出成bundle.js。
webpack性能优化
1. 构建速度优化
- resolve:可以加快文件读取速度,因为会影响tree-shaking,所有对整体性比较强的库设置resolve,如react.min.js。
resolve: {
// 默认值为node_modules,直接到node_modules里找依赖包,避免层层查找。
modules: [path.resolve(__dirname, 'node_modules')],
// 设置尽量少的值可以减少入口文件的搜索步骤
mainFields: ['main'],
// 使webpack直接使用库的min文件,避免库内解析
alias: {
'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
}
}
- noParse:字段告诉Webpack不必解析哪些文件,可以用来排除对非模块化库文件的解析。
module: {
noParse:[/jquery|chartjs/, /react\.min\.js$/],
}
-
配置loader时,通过test、exclude、include缩小搜索范围
匹配.js文件的时候,需要加 babel-loader,用来转ES6的代码。exclude用来排除打包某些文件中的代码。
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
}
]
},
-
使用DllPlugin减少基础模块编译次数,原理是把依赖的基础模块抽离出来打包到dll文件中,如react、react-dom,只要这些模块版本不升级,就只需被编译一次。
- 使用DllPlugin配置一个webpack.dll.config.js来构建dll文件
// webpack.dll.config.js const path = require('path'); const DllPlugin = require('webpack/lib/DllPlugin'); module.exports = { entry:{ react:['react','react-dom'], polyfill:['core-js/fn/promise','whatwg-fetch'] }, output:{ filename:'[name].dll.js', path:path.resolve(__dirname, 'dist'), library:'_dll_[name]', //dll的全局变量名 }, plugins:[ new DllPlugin({ name:'_dll_[name]', //dll的全局变量名 path:path.join(__dirname,'dist','[name].manifest.json'), //描述生成的manifest文件 }) ] }
需要注意DllPlugin的参数中name值必须和output.library值保持一致,并且生成的manifest文件中会引用output.library值。
最终构建出的文件:
|-- polyfill.dll.js |-- polyfill.manifest.json |-- react.dll.js └── react.manifest.json
- 在主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件
//webpack.config.json const path = require('path'); const DllReferencePlugin = require('webpack/lib/DllReferencePlugin'); module.exports = { entry:{ main:'./main.js' }, //... 省略output、loader等的配置 plugins:[ new DllReferencePlugin({ manifest:require('./dist/react.manifest.json') }), new DllReferenctPlugin({ manifest:require('./dist/polyfill.manifest.json') }) ] }
最终构建生成main.js
-
使用HappyPack开启多进程Loader转换
在整个构建流程中,最耗时的就是Loader对文件的转换操作了,而运行在Node.js之上的Webpack是单线程模型 的,也就是只能一个一个文件进行处理,不能并行处理。HappyPack可以将任务分解给多个子进程,最后将结果发给主进程。JS是单线程模型,只能通过这种多进程的方式提高性能。
npm i -D happypack // webpack.config.json const path = require('path'); const HappyPack = require('happypack'); module.exports = { //... module:{ rules:[ { test:/\.js$/, use:['happypack/loader?id=babel'] exclude:path.resolve(__dirname, 'node_modules') }, { test:/\.css/, use:['happypack/loader?id=css'] } ], plugins:[ new HappyPack({ id:'babel', loaders:['babel-loader?cacheDirectory'] }), new HappyPack({ id:'css', loaders:['css-loader'] }) ] } }
-
使用ParallelUglifyPlugin开启多进程压缩JS文件
2. 优化开发体验
开发过程中修改源码后,需要自动构建和刷新浏览器,以查看效果。这个过程可以使用Webpack实现自动化,Webpack负责监听文件的变化,DevServer负责刷新浏览器。
-
DevServer使用自动刷新
-
开启模块热替换HMR
3. 优化输出质量 - 压缩文件体积
-
UglifyJSPlugin:去掉JS中无效代码、去掉日志输入代码、缩短变量名等优化。
-
UglifyESPlugin:直接运行ES6代码时,对ES6代码进行压缩。
-
压缩CSS:
css-loader?minimize
PurifyCSSPlugin
-
启动Tree Shaking:
剔除用不上的死代码,它正常工作的前提是代码必须采用ES6的模块化语法
。
4. 优化输出质量 - 加速网络请求
4.1 使用CDN加速静态资源加载
-
CND加速的原理
CDN通过将资源部署到世界各地,使得用户可以就近访问资源,加快访问速度。要接入CDN,需要把网页的静态资源上传到CDN服务上,在访问这些资源时,使用CDN服务提供的URL。
由于CDN会为资源开启长时间的缓存,例如用户从CDN上获取了index.html,即使之后替换了CDN上的index.html,用户那边仍会在使用之前的版本直到缓存时间过期。业界做法:
-
HTML文件:放在自己的服务器上且关闭缓存,不接入CDN。
-
静态的JS、CSS、图片等资源:开启CDN和缓存,同时文件名带上由内容计算出的Hash值,这样只要内容变化hash就会变化,文件名就会变化,就会被重新下载而不论缓存时间多长。
另外,HTTP1.x版本的协议下,浏览器会对于向同一域名并行发起的请求数限制在4~8个。那么把所有静态资源放在同一域名下的CDN服务上就会遇到这种限制,所以可以把他们分散放在不同的CDN服务上,例如JS文件放在js.cdn.com下,将CSS文件放在css.cdn.com下等。这样又会带来一个新的问题:增加了域名解析时间,这个可以通过dns-prefetch来解决 来缩减域名解析的时间。形如 //xx.com 这样的URL省略了协议,这样做的好处是,浏览器在访问资源时会自动根据当前URL采用的模式来决定使用HTTP还是HTTPS协议。
-
-
总之,构建需要满足以下几点:
-
静态资源导入的URL要变成指向CDN服务的绝对路径的URL
-
静态资源的文件名需要带上根据内容计算出的Hash值
-
不同类型资源放在不同域名的CDN上
4.2 多页面应用提取页面间公共代码,以利用缓存
-
webpack4之前利用 CommonsChunkPlugin 插件来进行公共模块提取
-
webpack4之后利用 SplitChunksPlugin 插件来进行公共模块提取
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
name: "vendor",
test: /[\\/]node_modules[\\/]/,
chunks: "all",
priority: 10 // 优先级
},
common: {
name: "common",
test: /[\\/]src[\\/]/,
minSize: 1024,
chunks: "all",
priority: 5
}
}
}
},
};
5. 优化输出质量 - 提升代码运行时的效率
5.1 使用Prepack提前求值 (不太成熟)
5.2 使用Scope Hoisting
6. 使用输出分析工具webpack-bundle-analyzer
7、其他Tips
-
配置babel-loader时,use:
[‘babel-loader?cacheDirectory’]
cacheDirectory用于缓存babel的编译结果,加快重新编译的速度。另外注意排除node_modules文件夹,因为文件都使用了ES5的语法,没必要再使用Babel转换。 -
配置externals,排除因为已使用
<script>
标签引入而不用打包的代码,noParse是排除没使用模块化语句的代码。假设我们开发了一个自己的库,里面引用了lodash这个包,经过webpack打包的时候,发现如果把这个lodash包打入进去,打包文件就会非常大。那么我们就可以externals的方式引入。也就是说,自己的库本身不打包这个lodash,需要用户环境提供。
externals: { "lodash": { commonjs: "lodash", //如果我们的库运行在Node.js环境中,import _ from 'lodash'等价于const _ = require('lodash') commonjs2: "lodash", //同上 amd: "lodash", //如果我们的库使用require.js等加载,等价于 define(["lodash"], factory); root: "_" //如果我们的库在浏览器中使用,需要提供一个全局的变量‘_’,等价于 var _ = (window._) or (_); } }
总得来说,externals配置就是为了使import _ from 'lodash'这句代码,在本身不引入lodash的情况下,能够在各个环境都能解释执行。
-
配置performance参数可以输出文件的性能检查配置。
-
配置profile:true,是否捕捉Webpack构建的性能信息,用于分析是什么原因导致构建性能不佳。
-
配置cache:true,是否启用缓存来提升构建速度。
-
可以使用url-loader把小图片转换成base64嵌入到JS或CSS中,减少加载次数。
-
通过imagemin-webpack-plugin压缩图片,通过webpack-spritesmith制作雪碧图。
-
开发环境下将devtool设置为cheap-module-eval-source-map,因为生成这种source map的速度最快,能加速构建。在生产环境下将devtool设置为cheap-module-source-map。
webpack性能优化实践
加快打包速度
1> loader中exclude与include是用来排除或包含指定目录下的模块。 例如,babel-loader设置exclude node_modules,node_modules中的JS文件已经编译为ES5,没必要再进行转换。exclude和include同时存在,exclude优先级更高。
2> babel-loader中设置cacheDirectory,用于缓存babel的编译结果,加快重新编译的速度。
3> @babel/preset-env的modules配置项设置为false会禁用模块语句的转化,否则将导致tree-shaking失效。
4> commonsChunksPlugin 和 splitChunks 可以减少重模块打包,提升打包速度,减小资源体积。
提升请求效率
1> output中publicPath,可以使用CDN链接,用户可以更快地请求到资源
减小资源文件体积
1> uglify.js 和 terser.js