速度分析
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-plugin
uglifyjs-webpack-plugin
terser-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