webpack打包速度优化
前言
Webpack打包优化并没有什么固定的模式,一般我们常见的就是缓存、多进程、抽离、拆分。
一、分析打包速度
优化webpack构建速度的第一步就是知道时间花费在哪里,才可以集中的进行针对性的优化。
这边我们用到speed-measure-webpack-plugin
插件。
// 分析打包时间
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
// ...
module.exports = smp.wrap(prodWebpackConfig)
二、开销时间分析
如下是未优化前通过speed-measure-webpack-plugin
测量插件生成的数据分析:
SMP ⏱
General output time took 14 mins, 14.33 secs
SMP ⏱ Plugins
ExtractTextPlugin took 6 mins, 50.99 secs
PurifyPlugin took 6 mins, 11.47 secs
UglifyJsPlugin took 6 mins, 10.98 secs
NgcWebpackPlugin took 3 mins, 6.12 secs
LoaderOptionsPlugin took 35.55 secs
HtmlWebpackPlugin took 6.24 secs
ModuleConcatenationPlugin took 0.582 secs
HashedModuleIdsPlugin took 0.108 secs
CommonsChunkPlugin took 0.057 secs
Object took 0.031 secs
ScriptExtHtmlWebpackPlugin took 0.016 secs
HtmlElementsPlugin took 0.015 secs
DefinePlugin took 0.012 secs
InlineManifestPlugin took 0.01 secs
ProvidePlugin took 0.009 secs
SMP ⏱ Loaders
raw-loader took 6 mins, 17.56 secs
module count = 1688
css-loader took 6 mins, 17.053 secs
module count = 1291
css-loader, and
sass-loader took 4 mins, 53.45 secs
module count = 402
modules with no loaders took 3 mins, 20.57 secs
module count = 1736
file-loader took 1 min, 12.41 secs
module count = 13
@ngtools/webpack took 50.12 secs
module count = 3487
to-string-loader, and
css-loader took 14.66 secs
module count = 1285
json-loader took 4.9 secs
module count = 3
to-string-loader, and
css-loader, and
sass-loader took 3.83 secs
module count = 402
extract-text-webpack-plugin, and
style-loader, and
css-loader took 0.54 secs
module count = 4
html-webpack-plugin took 0.018 secs
module count = 1
从speed-measure-webpack-plugin
插件打包的时间分析可知,花销的总时间达到General output time took 14 mins, 14.33 secs14分钟之多,主要开销在loader的一些解析和编译操作上。
三、缓存处理
1. 通过cache-loader进行缓存处理
从开销时间来看,主要对开销较大的loader进行缓存操作。这边使用到cache-loader
进行缓存操作。
⚠️ 请注意,保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。
module: {
rules: [
...ngcWebpackConfig.loaders,
{
test: /\.css$/,
use: ['cache-loader', 'to-string-loader', 'css-loader'], // use loader是从右至左执行的, 因此必须放在执行的最后一个,也就是配置的第一个loader['cache-loader',...loaders]
exclude: [helpers.root('src', 'styles')]
},
{
test: /\.scss$/,
use: ['cache-loader', 'to-string-loader', 'css-loader', 'sass-loader'],
exclude: [helpers.root('src', 'styles')] // to-string-loader、raw-loader、file-loader进行缓存存在一些问题,实际中不适用
},
{
test: /\.html$/,
use: ['cache-loader', 'raw-loader'],
exclude: [helpers.root('src/index.html')]
},
{
test: /\.(jpg|png|gif)$/,
use: ['cache-loader', 'file-loader']
},
{
test: /\.(eot|woff2?|svg|ttf)([\?]?.*)$/,
use: ['cache-loader', 'file-loader']
}
]
}
2. UglifyJsPlugin开启压缩JavaScript代码
uglifyjs-webpack-plugin
插件也是我们常用用来压缩JavaScript代码的,在这个插件下也有一个参数用来配置缓存处理,也可以在压缩代码的时候生成缓存,以便在下次压缩代码的时候直接在缓存中取而不必重新压缩。
new UglifyJsPlugin({
cache: true // 开启压缩js代码缓存
})
⚠️ 请注意使用cache-loader
之后,我们再一次打包看看效果如何。因为第一次是没有生成缓存文件的,因此使用cache-loader
要在打包之后缓存过后的第一次打包,也就是加了cache-loader
之后的第二次打包才会生成缓存文件。
// 加了cache-loade, cache: true之后的第一次打包(生成缓存文件,但是第一次不走缓存,开销变大,因为加了一些cache-loader的解析和编译) 由原来的14分钟上升到17分钟。
SMP ⏱
General output time took 17 mins, 15.61 secs
SMP ⏱ Plugins
ExtractTextPlugin took 9 mins, 21.44 secs
PurifyPlugin took 8 mins, 39.02 secs
UglifyJsPlugin took 8 mins, 38.5 secs
NgcWebpackPlugin took 3 mins, 20.037 secs
LoaderOptionsPlugin took 38.75 secs
HtmlWebpackPlugin took 6.41 secs
ModuleConcatenationPlugin took 0.655 secs
HashedModuleIdsPlugin took 0.107 secs
CommonsChunkPlugin took 0.059 secs
Object took 0.032 secs
HtmlElementsPlugin took 0.016 secs
ScriptExtHtmlWebpackPlugin took 0.016 secs
InlineManifestPlugin took 0.011 secs
ProvidePlugin took 0.01 secs
DefinePlugin took 0.008 secs
SMP ⏱ Loaders
cache-loader, and
raw-loader took 6 mins, 33.38 secs
module count = 1688
cache-loader, and
to-string-loader, and
css-loader took 6 mins, 20.15 secs
module count = 1285
css-loader took 6 mins, 16.26 secs
module count = 1291
css-loader, and
sass-loader took 4 mins, 49.26 secs
module count = 402
modules with no loaders took 4 mins, 25.23 secs
module count = 1736
cache-loader, and
to-string-loader, and
css-loader, and
sass-loader took 4 mins, 6.97 secs
module count = 402
@ngtools/webpack took 48.6 secs
module count = 3487
cache-loader, and
file-loader took 14.78 secs
module count = 13
json-loader took 7.36 secs
module count = 3
extract-text-webpack-plugin, and
style-loader, and
css-loader took 0.528 secs
module count = 4
html-webpack-plugin took 0.019 secs
module count = 1
// 第二次构建,走缓存。花销时间为8分钟40s,可以看到是有很大的提升的。
SMP ⏱
General output time took 8 mins, 40.22 secs
SMP ⏱ Plugins
NgcWebpackPlugin took 3 mins, 20.82 secs
ExtractTextPlugin took 42.63 secs
LoaderOptionsPlugin took 38.24 secs
HtmlWebpackPlugin took 11.55 secs
UglifyJsPlugin took 1.74 secs
PurifyPlugin took 1.012 secs
ModuleConcatenationPlugin took 0.637 secs
HashedModuleIdsPlugin took 0.1 secs
CommonsChunkPlugin took 0.045 secs
Object took 0.034 secs
HtmlElementsPlugin took 0.015 secs
ScriptExtHtmlWebpackPlugin took 0.013 secs
InlineManifestPlugin took 0.01 secs
ProvidePlugin took 0.005 secs
DefinePlugin took 0.005 secs
SMP ⏱ Loaders
cache-loader, and
raw-loader took 6 mins, 31.28 secs
module count = 1688
cache-loader, and
to-string-loader, and
css-loader took 6 mins, 25.46 secs
module count = 1285
css-loader took 6 mins, 19.084 secs
module count = 1291
css-loader, and
sass-loader took 4 mins, 46.74 secs
module count = 402
cache-loader, and
to-string-loader, and
css-loader, and
sass-loader took 4 mins, 14.074 secs
module count = 402
modules with no loaders took 4 mins, 0.806 secs
module count = 1736
@ngtools/webpack took 49.95 secs
module count = 3487
cache-loader, and
file-loader took 18.81 secs
module count = 13
json-loader took 5.15 secs
module count = 3
extract-text-webpack-plugin, and
style-loader, and
css-loader took 0.411 secs
module count = 4
html-webpack-plugin took 0.016 secs
module count = 1
四、开启多进程
1. 使用happypack开启多进程解析和处理。
在使用 Webpack 对项目进行构建时,会对大量文件进行解析和处理。当文件数量变多之后,Webpack 构件速度就会变慢。由于运行在 Node.js 之上的 Webpack 是单线程模型的,所以 Webpack 需要处理的任务要一个一个进行操作。
而 Happypack 的作用就是将文件解析任务分解成多个子进程并发执行。子进程处理完任务后再将结果发送给主进程。所以可以大大提升 Webpack 的项目构件速度
cnpm i happypack --save-dev // 安装happypack
// webpack.config.js
const HappyPack = require('happypack'); // 通过node CommonJs模块导入
const os = require('os');
plugins: [
new HappyPack({
id: 'css',
threads: os.cpus().length,
loaders: ['to-string-loader', 'css-loader']
}),
new HappyPack({
id: 'scss', // 定义id为需开启happypack解析的loader进行标记
threads: os.cpus().length, // 开启操作系统cpu的最大核心数
loaders: ['to-string-loader', 'css-loader', 'sass-loader']
})
],
module: {
rules: [
{
test: /\.css$/,
use: 'happypack/loader?id=css' // id=css就是上述happypack定义的id
},
{
test: /\.scss$/,
use: 'happypack/loader?id=scss'
}
]
}
2. 同时使用cache-loader和happypack进行打包优化。
// 要同时使用cache-loader和happypack,只需要在happypack插件中的loaders配置把cache-loader加载配置最前头就可以,如下。
plugins: [
new HappyPack({
id: 'scss', // 定义id为需开启happypack解析的loader进行标记
threads: os.cpus().length, // 开启操作系统cpu的最大核心数
loaders: ['cache-loader', 'style-loader', 'css-loader', 'sass-loader'] // cache-loader最前
})
],
module: {
rules: [
{
test: /\.scss$/,
use: 'happypack/loader?id=scss'
}
]
}
3. UglifyJsPlugin开启多进程压缩JavaScript代码
new UglifyJsPlugin({
cache: true,
parallel: true // 开启多进程压缩js代码
})
// 同时使用缓存和多进程,可以看到进一步的提升
SMP ⏱
General output time took 7 mins, 35.03 secs
SMP ⏱ Plugins
NgcWebpackPlugin took 3 mins, 17.25 secs
ExtractTextPlugin took 43.79 secs
LoaderOptionsPlugin took 39.64 secs
HtmlWebpackPlugin took 4.98 secs
UglifyJsPlugin took 1.83 secs
PurifyPlugin took 1.018 secs
ModuleConcatenationPlugin took 0.717 secs
HappyPlugin took 0.364 secs
Object took 0.117 secs
HashedModuleIdsPlugin took 0.108 secs
CommonsChunkPlugin took 0.051 secs
HtmlElementsPlugin took 0.016 secs
ScriptExtHtmlWebpackPlugin took 0.015 secs
InlineManifestPlugin took 0.012 secs
ProvidePlugin took 0.008 secs
DefinePlugin took 0.008 secs
SMP ⏱ Loaders
happypack took 5 mins, 30.95 secs
module count = 1687
cache-loader, and
raw-loader took 6 mins, 28.28 secs
module count = 1688
modules with no loaders took 4 mins, 15.065 secs
module count = 1736
css-loader, and
sass-loader took 2 mins, 31.25 secs
module count = 402
@ngtools/webpack took 49.56 secs
module count = 3487
css-loader took 28.089 secs
module count = 1291
cache-loader, and
file-loader took 17.45 secs
module count = 13
json-loader took 6.14 secs
module count = 3
extract-text-webpack-plugin, and
style-loader, and
css-loader took 0.509 secs
module count = 4
五、抽离和外部引用Externals
项目中的抽离一般把不常改变的第三方依赖通过动态链接库抽离出来,再去引用,这样可以解决从动态链接库直接引用,减少了第三方包的打包时间,从而提高打包速度。
这边用到的插件是DllPlugin
和DLLReferencePlugin
。
- 使用
DllPlugin
需要先创建一个webpack.dll.config.js - 文件写入需要抽离的第三方包,如下的vendor: []
// webpack.dll.config.js
module.exports = {
entry: {
vendor: ["immutable", "ng-drag-drop", "ng-zorro-antd", "ngx-amap", "ngx-clipboard", "ngx-color-picker", "ngx-echarts", "ngx-infinite-scroll", "ngx-ueditor", "ngx-umeditor"]
},
output: {
path: path.resolve(__dirname, '../dll'),
filename: '[name].dll.js',
library: '[name]_[hash]'
},
plugins: [
new webpack.DllPlugin({
path: path.resolve(__dirname, '../dll', 'manifeset.json'),
name: '[name]_[hash]'
})
]
}
// package.json中配置
{
"scripts": {
"build:dll": "npm run webpack -- --config config/webpack.dll.config.js --progress",
}
}
// 执行npm run build:dll生成vendor.dll.js和manifeset.json文件
- 在webpack.prod.js生产配置文件中通过
DLLReferencePlugin
去引用对应的生成的manifeset.json文件
// webpack.prod.js
plugins: [
new webpack.DllReferencePlugin({
manifest: require('../dll/manifeset.json'),
})
]
- 也可以使用
externals
外部扩展,来从输出的bundle中排除依赖。然后在index.html通过script标签引用。这个不仅可以提升打包的速度,也可以缩小bundle的体积,从而提升http请求的速度来提高首屏渲染速度。
// webpack.prod.js
module.exports = {
externals: {
jquery: 'jQuery',
...obj
}
}
<!--index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>externals</title>
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
</head>
<body>
</body>
</html>
// index.js
import $ from 'jquery';
console.log($); // 输出成功,代表配置成功
- ⚠️但是要注意的是,在使用
Externals
的时候,如果我们更新了我们的代码依赖包的话,比如说通过npm undate <package name>
更新的话,我们还要把我们的第三方依赖包更新一下版本在通过script去引入,比较麻烦。可以通过一些webpack插件去自动生成CDN Link Script标签。(有兴趣的话可以继续去了解)。 - ⚠️还有一点是使用
DllPlugin
生成动态链接库的时候,在我们更新第三方依赖的时候,如果涉及到我们抽离出来的第三方包的话,我们首先要执行npm run build:dll
重新生成manifest.json,才不会出错。
六、微前端实践
管理后台项目较大,每次打包的时候都需要把管理后台重新打包,时间话费巨大。
因此可以把每一个子模块都被拆分成了一个单独的repo,通过微前端架构实践通过portal配置不同的模块入口。来把不同模块的分开打包,最后生成一个整体的代码结构。有兴趣的可以去看看。
需要考虑的是:
- 如何解决路由的问题
- 公共模块如何维护的问题
- ......等一些问题
七、总结
综上所述,一般构建优化优化措施的两个大方向:缓存(cache-loader)、多线程(happy-pack)。缓存是为了让二次构建时,不再去做重复的工作;而多核,是利用了硬件本身的优势,让我们充分发挥出cpu的性能。当然随着webpack的发展,会涌现出更多的优化插件以及方向。