webpack
的打包优化一直是个老生常谈的话题,常规的无非就分块、拆包、压缩等。html
本文以我本身的经验向你们分享如何经过一些分析工具、插件以及webpack
新版本中的一些新特性来显著提高webpack
的打包速度和改善包体积,学会分析打包的瓶颈以及问题所在。前端
本文演示代码,仓库地址vue
webpack 有时候打包很慢,而咱们在项目中可能用了不少的 plugin
和 loader
,想知道究竟是哪一个环节慢,下面这个插件能够计算 plugin
和 loader
的耗时。node
yarn add -D speed-measure-webpack-plugin
复制代码
配置也很简单,把 webpack
配置对象包裹起来便可:react
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin(); const webpackConfig = smp.wrap({ plugins: [ new MyPlugin(), new MyOtherPlugin() ] }); 复制代码
来看下在项目中引入speed-measure-webpack-plugin
后的打包状况: 从上图能够看出这个插件主要作了两件事情:jquery
loader
和
plugin
的耗时状况,咱们就能够“对症下药”了
打包后的体积优化是一个能够着重优化的点,好比引入的一些第三方组件库过大,这时就要考虑是否须要寻找替代品了。webpack
这里采用的是webpack-bundle-analyzer
,也是我平时工做中用的最多的一款插件了。git
它能够用交互式可缩放树形图显示webpack
输出文件的大小。用起来很是的方便。es6
首先安装插件:github
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) ] } ] } } 复制代码
yarn add -D happypack
复制代码
HappyPack
可让 Webpack
同一时间处理多个任务,发挥多核 CPU
的能力,将任务分解给多个子进程去并发的执行,子进程处理完后,再把结果发送给主进程。经过多进程模型,来加速代码构建。
// webpack.config.js
const 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
上面介绍的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.js
module.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.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); module.exports = { entry: // ... output: // ... plugins: [ new HardSourceWebpackPlugin() ] } 复制代码
❝❞
webpack5
中会内置hard-source-webpack-plugin
。
有时候咱们的项目中会用到不少模块,但有些模块实际上是不须要被解析的。这时咱们就能够经过缩小构建目标或者减小文件搜索范围的方式来对构建作适当的优化。
主要是exclude
与 include
的使用:
// webpack.config.js
const 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.js
const 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
前,咱们先来看下什么是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 直接返回浏览器所需的 polyfills
https://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.js
const 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,拉你进技术交流群一块儿学习 🍻
本文使用 mdnice 排版