Webpack Optimization
本篇目着重于 Webpack 速度及性能优化。暂时记录下 webpack V2版本相关的性能及构建用时优化,webpack V4版本相关优化后续补充。
起因
中后台应用中因npm包体量重、模块众多、组件颗粒度细腻,导致开发时遇到两大突出问题:
热更新速度缓慢,长达10s左右
构建用时过长,长达200s左右
热更新
在 Webpack v2版本中,当项目模块( chunks )众多时,热更新耗时主要用于 Webpack 内置插件 RemoveParentModulesPlugin 。
具体详情可参阅:RemoveParentModulesPlugin takes a long time with hundreds of chunks
阅读上述 issue ,可知 RemoveParentModulesPlugin 在 Webpack V2版本中会存在性能问题,在进一步查阅 Webpack 2.7 版本源码后,更加确认了此问题的存在。相关源码如下:
class RemoveParentModulesPlugin {
apply(compiler) {
compiler.plugin("compilation", (compilation) => {
compilation.plugin(["optimize-chunks-basic", "optimize-extracted-chunks-basic"], (chunks) => {
for(var index = 0; index < chunks.length; index++) {
var chunk = chunks[index];
if(chunk.parents.length === 0) continue;
// TODO consider Map when performance has improved https://gist.github.com/sokra/b36098368da7b8f6792fd7c85fca6311
var cache = Object.create(null);
var modules = chunk.modules.slice();
for(var i = 0; i < modules.length; i++) {
var module = modules[i];
var dId = debugIds(module.chunks);
var parentChunksWithModule;
if((dId in cache) && dId !== "no") {
parentChunksWithModule = cache[dId];
} else {
parentChunksWithModule = cache[dId] = allHaveModule(chunk.parents, module);
}
if(parentChunksWithModule) {
module.rewriteChunkInReasons(chunk, parentChunksWithModule);
chunk.removeModule(module);
}
}
}
});
});
}
}
因项目迁移至 Webpack V4版本,成本巨大,具有风险,只能在 Webpack V2版本进行打补丁式处理。
而在开发模式下,其实并不需要 RemoveParentModulesPlugin 提供的优化,故可以使用自定义插件来避免此次优化。相关代码如下:
class OptimizeChunkTraversePlugin{
apply(compiler) {
// apply the plugin in webpack < 4
// webpack4+ fixed this performance bug
// Reference: https://github.com/webpack/webpack/issues/6248
if (!compiler.hooks) {
compiler.plugin('compilation', compilation => {
// optimize webpack built-in plugin : RemoveParentModulesPlugin
compilation.plugin(['optimize-chunks-basic', 'optimize-extracted-chunks-basic'], chunks => {
chunks.forEach(chunk => {
chunk.parents = [];
})
})
})
}
}
}
通过此次优化,热更新耗时从 10s 下降至 2s,避免了之前开发过程中漫长等待。
构建时
在 Webpack v2版本中,关于构建时长的分析主要通过 webpack-bundle-analyzer 及 speed-measure-webpack-plugin 两个插件来进行,前者主要用于分析打包后各模块的体积和模块间的依赖关系,后者主要用于分析相关 plugin 及 loader 的运行时长。
优化前打包结果如下:
通过上述两个插件分析,针对项目进行了如下优化:
- 包体积
首先需要做得,即不常用的第三方库采用 CDN 来进行引入。
其次,针对于已引入并在项目中大量使用的库,为了避免代码变动导致的影响,采用 DllPlugin 来进行预打包,而为了区分开发与生产环境的差异性,引入了 autodll-webpack-plugin。
- 缓存
- loader 缓存,主要是开启 babel-loader 的 cache模式
{
test: /\.js$/,
loader: 'babel-loader?cacheDirectory=true',
include: [resolve('src')],
exclude: /node_modules/
}
uglify 缓存,主要是开启 UglifyJSPlugin 的 cache模式
docker( Jenkins )中 node_modules 缓存
- 多线程
happyPack:用于多线程加速 loader 解析
webpack-parallel-uglify-plugin:用于多线程加速 uglify 过程
优化后打包结果如下:
构建后
优化结束后,构建完出现下述错误:
ERROR in static/js/22.32e9e1b9c0f91bace1a9.js from UglifyJs
Unexpected token: name (hiddenTextarea) [static/js/22.32e9e1b9c0f91bace1a9.js:43,4]
参阅 SyntaxError: Unexpected token: name (xxxxxx) from Uglify plugin,得知 Uglify 并不能解析 ES6,因此抛出错误语法。
为解决此类问题,引入 babili-webpack-plugin 或 webpack-parallel-uglify-plugin 替换 Webpack 内置 UglifyJsPlugin 便可。
// 如下插件择一即可
const UglifyJSPlugin = require('webpack-parallel-uglify-plugin')
const BabiliPlugin = require("babili-webpack-plugin");
plugins:[
...
new BabiliPlugin(),
new UglifyJSPlugin({
uglifyES: {}
})
...
]
上线后
优化项部署到生产环境后,中后台控制台出现以下错误:
参阅以下文章:
最初做了 element-ui 依赖 Vue 版本和项目中现有 Vue 版本统一尝试,发现并没有解决问题。
之后,考虑到可能是 Webpack 打包时索引 Vue 的问题,改变了索引项,如下:
import Vue from 'vue/dist/vue.esm.js'
发现依旧没有解决,两次尝试后,基本可以把问题聚焦于预打包后的 vendor 中 Vue 的问题。
当 Vue 全局化后,node_modules 下其他包有依赖其他版本的 Vue 时,打包后就会出现类似的问题,可以通过如下命令来查看依赖 Vue 的相关包。
npm ls vue
解决办法即是使用 AutoDllPlugin 预打包时不打包 Vue :
new AutoDllPlugin({
inject: true,
filename: '[name].js',
context: path.join(__dirname, '..'),
entry: {
vendor: [
- 'vue/dist/vue.esm.js',
'vuex',
'vue-router',
'axios',
'element-ui',
'moment'
]
},
plugins:[
new webpack.optimize.UglifyJsPlugin()
]
})