速度分析
webpack 有时候打包很慢,而我们在项目中可能用了很多的 plugin 和 loader,想知道到底是哪个环节慢,下面这个插件可以计算 plugin 和 loader 的耗时。
yarn add -D speed-measure-webpack-plugin配置
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin()
const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()]
})这个插件主要做了两件事情:
- 计算整个打包总耗时
- 分析每个插件和
loader的耗时情况
体积分析
yarn add -D webpack-bundle-analyzer配置
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'
})
]
}多进程
webpack 是运行在 node 环境中,而 node 是单线程的。webpack 的打包过程是 io 密集和计算密集型的操作,如果能同时 fork 多个进程并行处理各个任务,将会有效的缩短构建时间。
最常用的是thread-loader和HappyPack ,其中thread-loader吧,这个也是webpack4官方所推荐的。
yarn add -D thread-loader配置
module.exports = {
module: {
rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
'thread-loader'
// your expensive loader (e.g babel-loader)
]
}
]
}
}thread-loader 会将你的 loader 放置在一个 worker 池里面运行,以达到多线程构建。
thread-loader和happypack对于小型项目来说打包速度几乎没有影响,甚至可能会增加开销,所以建议尽量在大项目中采用。
多进程并行压缩代码
通常我们在开发环境,代码构建时间比较快,而构建用于发布到线上的代码时会添加压缩代码这一流程,则会导致计算量大耗时多。
webpack默认提供了UglifyJS插件来压缩JS代码,但是它使用的是单线程压缩代码,也就是说多个 js 文件需要被压缩,它需要一个个文件进行压缩。所以说在正式环境打包压缩代码速度非常慢(因为压缩JS代码需要先把代码解析成用Object抽象表示的AST语法树,再应用各种规则分析和处理AST,导致这个过程耗时非常大)。
常用的做法就是多进程并行压缩
parallel-uglify-pluginuglifyjs-webpack-pluginterser-webpack-plugin
parallel-uglify-plugin
当webpack有多个JS文件需要输出和压缩时,原来会使用UglifyJS去一个个压缩并且输出,而ParallelUglifyPlugin插件则会开启多个子进程,把对多个文件压缩的工作分给多个子进程去完成,但是每个子进程还是通过UglifyJS去压缩代码。并行压缩可以显著的提升效率。
uglifyjs-webpack-plugin
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
plugins: [
new UglifyJsPlugin({
uglifyOptions: {
warnings: false,
parse: {},
compress: {},
ie8: false
},
parallel: true
})
]
}设置parallel: true开启多进程压缩
terser-webpack-plugin
webpack4 已经默认支持 ES6语法的压缩。
而这离不开terser-webpack-plugin
const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: 4
})
]
}
}Externals
将一些JS文件存储在 CDN 上,在 index.html 中通过 <script> 标签引入
使用import方式导入且webpack不会打包
//webpack.config.js
module.exports = {
//...
externals: {
//jquery通过script引入之后,全局中即有了 jQuery 变量
jquery: 'jQuery'
}
}预编译资源模块
在使用webpack进行打包时候,对于依赖的第三方库,比如vue,vuex等这些不会修改的依赖,我们可以让它和我们自己编写的代码分开打包,这样做的好处是每次更改我本地代码的文件的时候,webpack只需要打包我项目本身的文件代码,而不会再去编译第三方库。
那么第三方库在第一次打包的时候只打包一次,以后只要我们不升级第三方包的时候,那么webpack就不会对这些库去打包,这样的可以快速的提高打包的速度。其实也就是预编译资源模块。
webpack中,我们可以结合DllPlugin 和 DllReferencePlugin插件来实现。
DllPlugin
DllPlugin能把第三方库代码分离开,并且每次文件更改的时候,它只会打包该项目自身的代码。所以打包速度会更快。
DLLPlugin 插件是在一个额外独立的webpack设置中创建一个只有dll的bundle,也就是说我们在项目根目录下除了有webpack.config.js,还会新建一个webpack.dll.js文件。
webpack.dll.js的作用是把所有的第三方库依赖打包到一个bundle的dll文件里面,还会生成一个名为 manifest.json文件。该manifest.json的作用是用来让 DllReferencePlugin 映射到相关的依赖上去的。
DllReferencePlugin
什么意思呢?就是说在webpack.dll.js中打包后比如会生成 vendor.dll.js文件和vendor-manifest.json文件,vendor.dll.js文件包含了所有的第三方库文件,vendor-manifest.json文件会包含所有库代码的一个索引,当在使用webpack.config.js文件打包DllReferencePlugin插件的时候,会使用该DllReferencePlugin插件读取vendor-manifest.json文件,看看是否有该第三方库
使用方法:
使用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其中xx.dll.js包含打包的 n 多模块,这些模块存在一个数组里,并以数组索引作为 ID,通过一个变量假设为_xx_dll暴露在全局中,可以通过window._xx_dll访问这些模块。xx.manifest.json文件描述 dll 文件包含哪些模块、每个模块的路径和 ID。然后再在项目的主config文件里使用DllReferencePlugin插件引入xx.manifest.json文件
引入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
利用缓存提升二次构建速度
在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,以将结果缓存到磁盘里。
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()
]
}缩小构建目标/减少文件搜索范围
// 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 的解析,提升构建速度
// 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'] //限定模块入口文件名resolve.modules:告诉webpack解析模块时应该搜索的目录resolve.mainFields:当从npm包中导入模块时(例如,import * as React from 'react'),此选项将决定在package.json中使用哪个字段导入模块。根据webpack配置中指定的target不同,默认值也会有所不同resolve.mainFiles:解析目录时要使用的文件名,默认是indexresolve.extensions:文件扩展名
动态 Polyfill 服务
babel只负责语法转换,比如将ES6的语法转换成ES5。但如果有些对象、方法,浏览器本身不支持,比如:
- 全局对象:
Promise、WeakMap等。 - 全局静态函数:
Array.from、Object.assign等。 - 实例方法:比如
Array.prototype.includes等。
此时,需要引入babel-polyfill来模拟实现这些对象、方法。
使用
module.exports = {
entry: ['@babel/polyfill', './app/js']
}由于是一次性全部导入整个polyfill,所以导致了一个大问题:文件很大。为了解决这个问题,引入动态polyfill。
每次打开页面,浏览器都会向Polyfill Service发送请求,Polyfill Service识别 User Agent,下发不同的 Polyfill,做到按需加载Polyfill的效果
采用官方提供的服务地址 https://polyfill.io/v3/polyfill.min.js
Scope Hoisting
Scope hoisting 直译过来就是「作用域提升」。熟悉 JavaScript 都应该知道「函数提升」和「变量提升」,JavaScript 会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack 会把引入的 js 文件“提升到”它的引入者顶部。
Scope Hoisting 可以让 Webpack 打包出来的代码文件更小、运行的更快。
要在 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()]
}
}