引言
webpack
的打包优化始终是个陈词滥调的话题,惯例的无非就分块、拆包、压缩等。
本文以我本人的教训向大家分享如何通过一些剖析工具、插件以及webpack
新版本中的一些新个性来显著晋升webpack
的打包速度和改善包体积,学会剖析打包的瓶颈以及问题所在。
本文演示代码,仓库地址
速度剖析 ????
webpack 有时候打包很慢,而咱们在我的项目中可能用了很多的 plugin
和 loader
,想晓得到底是哪个环节慢,上面这个插件能够计算 plugin
和 loader
的耗时。
yarn add -D speed-measure-webpack-plugin
配置也很简略,把 webpack
配置对象包裹起来即可:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");const smp = new SpeedMeasurePlugin();const webpackConfig = smp.wrap({ plugins: [ new MyPlugin(), new MyOtherPlugin() ]});
来看下在我的项目中引入speed-measure-webpack-plugin
后的打包状况:
从上图能够看出这个插件次要做了两件事件:
- 计算整个打包总耗时
- 剖析每个插件和 loader 的耗时状况
晓得了具体loader
和plugin
的耗时状况,咱们就能够“隔靴搔痒”了
体积剖析 ????
打包后的体积优化是一个能够着重优化的点,比方引入的一些第三方组件库过大,这时就要思考是否须要寻找替代品了。
这里采纳的是webpack-bundle-analyzer
,也是我平时工作中用的最多的一款插件了。
它能够用交互式可缩放树形图显示webpack
输入文件的大小。用起来十分的不便。
首先装置插件:
yarn add -D webpack-bundle-analyzer
装置完在webpack.config.js
中简略的配置一下:
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;module.exports = { plugins: [ new BundleAnalyzerPlugin({ // 能够是`server`,`static`或`disabled`。 // 在`server`模式下,分析器将启动HTTP服务器来显示软件包报告。 // 在“动态”模式下,会生成带有报告的单个HTML文件。 // 在`disabled`模式下,你能够应用这个插件来将`generateStatsFile`设置为`true`来生成Webpack Stats JSON文件。 analyzerMode: "server", // 将在“服务器”模式下应用的主机启动HTTP服务器。 analyzerHost: "127.0.0.1", // 将在“服务器”模式下应用的端口启动HTTP服务器。 analyzerPort: 8866, // 门路捆绑,将在`static`模式下生成的报告文件。 // 绝对于捆绑输入目录。 reportFilename: "report.html", // 模块大小默认显示在报告中。 // 应该是`stat`,`parsed`或者`gzip`中的一个。 // 无关更多信息,请参见“定义”一节。 defaultSizes: "parsed", // 在默认浏览器中主动关上报告 openAnalyzer: true, // 如果为true,则Webpack Stats JSON文件将在bundle输入目录中生成 generateStatsFile: false, // 如果`generateStatsFile`为`true`,将会生成Webpack Stats JSON文件的名字。 // 绝对于捆绑输入目录。 statsFilename: "stats.json", // stats.toJson()办法的选项。 // 例如,您能够应用`source:false`选项排除统计文件中模块的起源。 // 在这里查看更多选项:https: //github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21 statsOptions: null, logLevel: "info" ) ]}
而后在命令行工具中输出npm run dev
,它默认会起一个端口号为 8888 的本地服务器:
图中的每一块清晰的展现了组件、第三方库的代码体积。
有了它,咱们就能够针对体积偏大的模块进行相干优化了。
多过程/多实例构建 ????
大家都晓得 webpack
是运行在 node
环境中,而 node
是单线程的。webpack
的打包过程是 io
密集和计算密集型的操作,如果能同时 fork
多个过程并行处理各个工作,将会无效的缩短构建工夫。
平时用的比拟多的两个是thread-loader
和HappyPack
。
先来看下thread-loader
吧,这个也是webpack4
官网所举荐的。
thread-loader
装置
yarn add -D thread-loader
thread-loader
会将你的 loader
搁置在一个 worker
池外面运行,以达到多线程构建。
把这个loader
搁置在其余loader
之前(如上面示例的地位), 搁置在这个loader
之后的loader
就会在一个独自的worker
池(worker pool
)中运行。
示例
module.exports = { module: { rules: [ { test: /\.js$/, include: path.resolve("src"), use: [ "thread-loader", // your expensive loader (e.g babel-loader) ] } ] }}
HappyPack
装置
yarn add -D happypack
HappyPack
能够让 Webpack
同一时间解决多个工作,施展多核 CPU
的能力,将工作合成给多个子过程去并发的执行,子过程解决完后,再把后果发送给主过程。通过多过程模型,来减速代码构建。
示例
// webpack.config.jsconst HappyPack = require('happypack');exports.module = { rules: [ { test: /.js$/, // 1) replace your original list of loaders with "happypack/loader": // loaders: [ 'babel-loader?presets[]=es2015' ], use: 'happypack/loader', include: [ /* ... */ ], exclude: [ /* ... */ ] } ]};exports.plugins = [ // 2) create the plugin: new HappyPack({ // 3) re-add the loaders you replaced above in #1: loaders: [ 'babel-loader?presets[]=es2015' ] })];
这里有一点须要阐明的是,HappyPack
的作者示意已不再保护此我的项目,这个能够在github
仓库看到:
作者也是举荐应用webpack
官网提供的thread-loader
。
thread-loader
和happypack
对于小型我的项目来说打包速度简直没有影响,甚至可能会减少开销,所以倡议尽量在大我的项目中采纳。
多过程并行压缩代码 ????
通常咱们在开发环境,代码构建工夫比拟快,而构建用于公布到线上的代码时会增加压缩代码这一流程,则会导致计算量大耗时多。
webpack
默认提供了UglifyJS
插件来压缩JS
代码,然而它应用的是单线程压缩代码,也就是说多个js
文件须要被压缩,它须要一个个文件进行压缩。所以说在正式环境打包压缩代码速度十分慢(因为压缩JS
代码须要先把代码解析成用Object
形象示意的AST
语法树,再利用各种规定剖析和解决AST
,导致这个过程耗时十分大)。
所以咱们要对压缩代码这一步骤进行优化,罕用的做法就是多过程并行压缩。
目前有三种支流的压缩计划:
- parallel-uglify-plugin
- uglifyjs-webpack-plugin
- terser-webpack-plugin
parallel-uglify-plugin
下面介绍的HappyPack
的思维是应用多个子过程去解析和编译JS
,CSS
等,这样就能够并行处理多个子工作,多个子工作实现后,再将后果发到主过程中,有了这个思维后,ParallelUglifyPlugin
插件就产生了。
当webpack
有多个JS
文件须要输入和压缩时,原来会应用UglifyJS
去一个个压缩并且输入,而ParallelUglifyPlugin
插件则会开启多个子过程,把对多个文件压缩的工作分给多个子过程去实现,然而每个子过程还是通过UglifyJS
去压缩代码。并行压缩能够显著的晋升效率。
装置
yarn add -D webpack-parallel-uglify-plugin
示例
import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';module.exports = { plugins: [ new ParallelUglifyPlugin({ // Optional regex, or array of regex to match file against. Only matching files get minified. // Defaults to /.js$/, any file ending in .js. test, include, // Optional regex, or array of regex to include in minification. Only matching files get minified. exclude, // Optional regex, or array of regex to exclude from minification. Matching files are not minified. cacheDir, // Optional absolute path to use as a cache. If not provided, caching will not be used. workerCount, // Optional int. Number of workers to run uglify. Defaults to num of cpus - 1 or asset count (whichever is smaller) sourceMap, // Optional Boolean. This slows down the compilation. Defaults to false. uglifyJS: { // These pass straight through to uglify-js@3. // Cannot be used with uglifyES. // Defaults to {} if not neither uglifyJS or uglifyES are provided. // You should use this option if you need to ensure es5 support. uglify-js will produce an error message // if it comes across any es6 code that it can't parse. }, uglifyES: { // These pass straight through to uglify-es. // Cannot be used with uglifyJS. // uglify-es is a version of uglify that understands newer es6 syntax. You should use this option if the // files that you're minifying do not need to run in older browsers/versions of node. } }), ],};
webpack-parallel-uglify-plugin
已不再保护,这里不举荐应用
uglifyjs-webpack-plugin
装置
yarn add -D uglifyjs-webpack-plugin
示例
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');module.exports = { plugins: [ new UglifyJsPlugin({ uglifyOptions: { warnings: false, parse: {}, compress: {}, ie8: false }, parallel: true }) ]};
其实它和下面的parallel-uglify-plugin
相似,也可通过设置parallel: true
开启多过程压缩。
terser-webpack-plugin
不晓得你有没有发现:webpack4
曾经默认反对 ES6
语法的压缩。
而这离不开terser-webpack-plugin
。
装置
yarn add -D terser-webpack-plugin
示例
const TerserPlugin = require('terser-webpack-plugin');module.exports = { optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: 4, }), ], },};
预编译资源模块 ????
什么是预编译资源模块?
在应用webpack
进行打包时候,对于依赖的第三方库,比方vue
,vuex
等这些不会批改的依赖,咱们能够让它和咱们本人编写的代码离开打包,这样做的益处是每次更改我本地代码的文件的时候,webpack
只须要打包我我的项目自身的文件代码,而不会再去编译第三方库。
那么第三方库在第一次打包的时候只打包一次,当前只有咱们不降级第三方包的时候,那么webpack
就不会对这些库去打包,这样的能够疾速的进步打包的速度。其实也就是预编译资源模块
。
webpack
中,咱们能够联合DllPlugin
和 DllReferencePlugin
插件来实现。
DllPlugin
是什么?
它能把第三方库代码分来到,并且每次文件更改的时候,它只会打包该我的项目本身的代码。所以打包速度会更快。
DLLPlugin
插件是在一个额定独立的webpack
设置中创立一个只有dll
的bundle
,也就是说咱们在我的项目根目录下除了有webpack.config.js
,还会新建一个webpack.dll.js
文件。
webpack.dll.js
的作用是把所有的第三方库依赖打包到一个bundle
的dll
文件外面,还会生成一个名为 manifest.json
文件。该manifest.json
的作用是用来让 DllReferencePlugin
映射到相干的依赖下来的。
DllReferencePlugin
又是什么?
这个插件是在webpack.config.js
中应用的,该插件的作用是把刚刚在webpack.dll.js
中打包生成的dll
文件援用到须要的预编译的依赖上来。
什么意思呢?就是说在webpack.dll.js
中打包后比方会生成 vendor.dll.js
文件和vendor-manifest.json
文件,vendor.dll.js
文件蕴含了所有的第三方库文件,vendor-manifest.json
文件会蕴含所有库代码的一个索引,当在应用webpack.config.js
文件打包DllReferencePlugin
插件的时候,会应用该DllReferencePlugin
插件读取vendor-manifest.json
文件,看看是否有该第三方库。
vendor-manifest.json
文件就是一个第三方库的映射而已。
怎么在我的项目中应用?
下面说了这么多,次要是为了不便大家对于预编译资源模块
和DllPlugin
和、DllReferencePlugin
插件作用的了解(我第一次应用看了良久才明确~~)
先来看下实现的我的项目目录构造:
次要在两块配置,别离是webpack.dll.js
和webpack.config.js
(对应这里我是webpack.base.js
)
webpack.dll.js
const path = require('path');const webpack = require('webpack');module.exports = { mode: 'production', entry: { vendors: ['lodash', 'jquery'], react: ['react', 'react-dom'] }, output: { filename: '[name].dll.js', path: path.resolve(__dirname, './dll'), library: '[name]' }, plugins: [ new webpack.DllPlugin({ name: '[name]', path: path.resolve(__dirname, './dll/[name].manifest.json') }) ]}
这里我拆了两局部:vendors
(寄存了lodash
、jquery
等)和react
(寄存了 react 相干的库,react
、react-dom
等)
webpack.config.js
(对应我这里就是webpack.base.js
)
const path = require("path");const fs = require('fs');// ...const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');const webpack = require('webpack');const plugins = [ // ...];const files = fs.readdirSync(path.resolve(__dirname, './dll'));files.forEach(file => { if(/.*\.dll.js/.test(file)) { plugins.push(new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, './dll', file) })) } if(/.*\.manifest.json/.test(file)) { plugins.push(new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, './dll', file) })) }})module.exports = { entry: { main: "./src/index.js" }, module: { rules: [] }, plugins, output: { // publicPath: "./", path: path.resolve(__dirname, "dist") }}
这里为了演示省略了很多代码,我的项目残缺代码在这里
因为下面我把第三方库做了一个拆分,所以对应生成也就会是多个文件,这里读取了一下文件,做了一层遍历。
最初在package.json
外面再增加一条脚本就能够了:
"scripts": { "build:dll": "webpack --config ./webpack.dll.js", },
运行yarn build:dll
就会生成本大节结尾贴的那张我的项目结构图了~
利用缓存晋升二次构建速度 ????
一般来说,对于动态资源,咱们都心愿浏览器可能进行缓存,那样当前进入页面就能够间接应用缓存资源,页面关上速度会显著放慢,既进步了用户的体验也节俭了宽带资源。
当然浏览器缓存办法有很多种,这里只简略探讨下在webpack
中如何利用缓存来晋升二次构建速度。
在webpack
中利用缓存个别有以下几种思路:
babel-loader
开启缓存- 应用
cache-loader
- 应用
hard-source-webpack-plugin
babel-loader
babel-loader
在执行的时候,可能会产生一些运行期间反复的公共文件,造成代码体积冗余,同时也会减慢编译效率。
能够加上cacheDirectory
参数开启缓存:
{ test: /\.js$/, exclude: /node_modules/, use: [{ loader: "babel-loader", options: { cacheDirectory: true } }], },
cache-loader
在一些性能开销较大的 loader
之前增加此 loader
,以将后果缓存到磁盘里。
装置
yarn add -D cache-loader
应用
cache-loader
的配置很简略,放在其余 loader
之前即可。批改Webpack
的配置如下:
// webpack.config.jsmodule.exports = { module: { rules: [ { test: /\.ext$/, use: [ 'cache-loader', ...loaders ], include: path.resolve('src') } ] }}
请留神,保留和读取这些缓存文件会有一些工夫开销,所以请只对性能开销较大的loader
应用此loader
。
hard-source-webpack-plugin
HardSourceWebpackPlugin
为模块提供了两头缓存,缓存默认的寄存门路是: node_modules/.cache/hard-source
。
配置 hard-source-webpack-plugin
后,首次构建工夫并不会有太大的变动,然而从第二次开始,构建工夫大概能够缩小 80%
左右。
装置
yarn add -D hard-source-webpack-plugin
应用
// webpack.config.jsvar HardSourceWebpackPlugin = require('hard-source-webpack-plugin');module.exports = { entry: // ... output: // ... plugins: [ new HardSourceWebpackPlugin() ]}
webpack5
中会内置hard-source-webpack-plugin
。
放大构建指标/缩小文件搜寻范畴 ????
有时候咱们的我的项目中会用到很多模块,但有些模块其实是不须要被解析的。这时咱们就能够通过放大构建指标或者缩小文件搜寻范畴的形式来对构建做适当的优化。
放大构建指标
次要是exclude
与 include
的应用:
- exclude: 不须要被解析的模块
- include: 须要被解析的模块
// webpack.config.jsconst path = require('path');module.exports = { ... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, // include: path.resolve('src'), use: ['babel-loader'] } ] }
这里babel-loader
就会排除对node_modules
下对应 js
的解析,晋升构建速度。
缩小文件搜寻范畴
这个次要是resolve
相干的配置,用来设置模块如何被解析。通过resolve
的配置,能够帮忙Webpack
疾速查找依赖,也能够替换对应的依赖。
resolve.modules
:通知webpack
解析模块时应该搜寻的目录resolve.mainFields
:当从npm
包中导入模块时(例如,import * as React from 'react'
),此选项将决定在package.json
中应用哪个字段导入模块。依据webpack
配置中指定的target
不同,默认值也会有所不同resolve.mainFiles
:解析目录时要应用的文件名,默认是index
resolve.extensions
:文件扩展名
// webpack.config.jsconst path = require('path');module.exports = { ... resolve: { alias: { react: path.resolve(__dirname, './node_modules/react/umd/react.production.min.js') }, //间接指定react搜寻模块,不设置默认会一层层的搜查 modules: [path.resolve(__dirname, 'node_modules')], //限定模块门路 extensions: ['.js'], //限定文件扩展名 mainFields: ['main'] //限定模块入口文件名
动静 Polyfill 服务 ????
介绍动静Polyfill
前,咱们先来看下什么是babel-polyfill
。
什么是 babel-polyfill?
babel
只负责语法转换,比方将ES6
的语法转换成ES5
。但如果有些对象、办法,浏览器自身不反对,比方:
- 全局对象:
Promise
、WeakMap
等。 - 全局动态函数:
Array.from
、Object.assign
等。 - 实例办法:比方
Array.prototype.includes
等。
此时,须要引入babel-polyfill
来模仿实现这些对象、办法。
这种个别也称为垫片
。
怎么应用babel-polyfill
?
应用也非常简单,在webpack.config.js
文件作如下配置就能够了:
module.exports = { entry: ["@babel/polyfill", "./app/js"],};
为什么还要用动静Polyfill
?
babel-polyfill
因为是一次性全副导入整个polyfill
,所以用起来很不便,但与此同时也带来了一个大问题:文件很大,所以后续的计划都是针对这个问题做的优化。
来看下打包后babel-polyfill
的占比:
占比 29.6%,有点太大了!
介于上述起因,动静Polyfill
服务诞生了。
通过一张图来理解下Polyfill Service
的原理:
每次关上页面,浏览器都会向Polyfill Service
发送申请,Polyfill Service
辨认 User Agent
,下发不同的 Polyfill
,做到按需加载Polyfill
的成果。
怎么应用动静Polyfill
服务?
采纳官网提供的服务地址即可:
//拜访url,依据User Agent 间接返回浏览器所需的 polyfillshttps://polyfill.io/v3/polyfill.min.js
Scope Hoisting
????
什么是Scope Hoisting
?
Scope hoisting
直译过去就是「作用域晋升」。相熟 JavaScript
都应该晓得「函数晋升」和「变量晋升」,JavaScript
会把函数和变量申明晋升到以后作用域的顶部。「作用域晋升」也相似于此,webpack
会把引入的 js
文件“晋升到”它的引入者顶部。
Scope Hoisting
能够让 Webpack
打包进去的代码文件更小、运行的更快。
启用Scope Hoisting
要在 Webpack
中应用 Scope Hoisting
非常简单,因为这是 Webpack
内置的性能,只须要配置一个插件,相干代码如下:
// webpack.config.jsconst webpack = require('webpack')module.exports = mode => { if (mode === 'production') { return {} } return { devtool: 'source-map', plugins: [new webpack.optimize.ModuleConcatenationPlugin()], }}
启用Scope Hoisting
后的比照
让咱们先来看看在没有 Scope Hoisting
之前 Webpack
的打包形式。
如果当初有两个文件别离是
constant.js
:
export default 'Hello,Jack-cool';
- 入口文件
main.js
:
import str from './constant.js';console.log(str);
以上源码用 Webpack
打包后的局部代码如下:
[ (function (module, __webpack_exports__, __webpack_require__) { var __WEBPACK_IMPORTED_MODULE_0__constant_js__ = __webpack_require__(1); console.log(__WEBPACK_IMPORTED_MODULE_0__constant_js__["a"]); }), (function (module, __webpack_exports__, __webpack_require__) { __webpack_exports__["a"] = ('Hello,Jack-cool'); })]
在开启 Scope Hoisting
后,同样的源码输入的局部代码如下:
[ (function (module, __webpack_exports__, __webpack_require__) { var constant = ('Hello,Jack-cool'); console.log(constant); })]
从中能够看出开启 Scope Hoisting
后,函数申明由两个变成了一个,constant.js
中定义的内容被间接注入到了 main.js
对应的模块中。 这样做的益处是:
- 代码体积更小,因为函数申明语句会产生大量代码;
- 代码在运行时因为创立的函数作用域更少了,内存开销也随之变小。
Scope Hoisting
的实现原理其实很简略:剖析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。 因而只有那些被援用了一次的模块能力被合并。
因为Scope Hoisting
须要剖析出模块之间的依赖关系,因而源码必须采纳ES6
模块化语句,不然它将无奈失效。
参考
极客工夫 【玩转 webpack】
❤️ 爱心三连击
1.如果感觉这篇文章还不错,就帮忙点赞、分享一下吧,让更多的人也看到~
2.关注公众号前端森林,定期为你推送陈腐干货好文。
3.非凡阶段,带好口罩,做好集体防护。
4.增加微信fs1263215592,拉你进技术交换群一起学习 ????