前言
webpack的出现为前端开发带来翻天覆地的变化,无论你是用React,Vue还是Angular,webpack都是主流的构建工具。我们每天都跟它打交道,但却很少主动去了解它,就像写字楼里的礼仪小姐姐,既熟悉又陌生。随着项目复杂度的上升,打包构建的时间会越来越长。终于有一天,你发现npm run dev
后,去泡个茶,上了个厕所,跟同事bb一轮后回到座位,项目还没构建完的时候,你就会下定决心好好了解下这个熟悉的陌生人。
这次优化的目的主要有两个:
- 加快编译构建速度
- 减少页面加载的时间
现状是每次开发模式构建,大概要花120秒;生产模式构建,大概要花300秒。项目总共有将近150个chunk。
加快编译构建速度
有2种方式可以加快编译的速度,分别是减少每次打包的文件数目,并行的去执行打包任务。这里用到了2个webpack插件:
- DllPlugin(减少每次打包的文件数目)
- HappyPack(并行的去执行打包任务)
下面对这两个插件作详细的介绍。
DllPlugin
dll是Dynamic Link Library(动态链接库)的缩写,是Windows系统共享函数库的一种方式。将一些比较少改变的库和工具,比如React、React-DOM,事先独立打包成一个chunk,以后每次构建的时候再直接导入,就不用每次都对这些文件打包了。这里有2个分解动作:
- 独立打包dll
- 导入dll
使用DllPlugin可以独立打包dll,具体的配置如下:
const path = require('path');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const env = process.env.NODE_ENV;
module.exports = {
entry: {
vendor: ['react', 'react-dom', 'react-router', 'redux', 'react-redux', 'redux-thunk'],
},
output: {
filename: '[name]_dll_[chunkhash].js',
path: path.resolve(__dirname, 'dll'),
library: '_dll_[name]',
},
resolve: {
mainFields: ['jsnext:main', 'browser', 'main'],
},
plugins: [
new webpack.DllPlugin({
name: '_dll_[name]',
path: path.join(__dirname, 'dll', '[name].manifest.json'),
}),
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify(env),
},
}),
new UglifyJSPlugin({
cache: true,
parallel: true,
exclude: [/node_modules/],
uglifyOptions: {
compress: {
warnings: false,
drop_console: true,
collapse_vars: true,
reduce_vars: true,
},
output: {
beautify: false,
comments: false,
},
},
}),
],
};
DllPlugin网上有一些例子,但都不完美,体现在以下2点:
- 没有压缩代码
- 没有hash,当依赖更新时无法通知浏览器更新缓存
第1点比较好处理,加上DefinePlugin和UglifyJSPlugin就可以了。处理第2点的时候,除了在output加上chunkhash,在引入dll的时候需要做一些额外的操作,下文会讲解。
这时在package.json加上一个命令,npm run dll
一下就会生成一个类似这样的文件:vendor_dll_be1f5270e490dcb25f.js
{
...
"scripts": {
"dll": "cross-env NODE_ENV=production webpack --config webpack.dll.js --progress"
}
...
}
dll生成后,就要在构建的配置文件里将其引入,这时候就用到DllReferencePlugin和AddAssetHtmlPlugin,配置如下
const fs = require('fs');
const path = require('path');
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');
const files = fs.readdirSync(path.resolve(__dirname, 'dll'));
const vendorFiles = files.filter(file => file.match(/vendor_dll_\w+.js/));
const vendorFile = vendorFiles[0];
module.exports = {
...
plugins: [
...
new webpack.DllReferencePlugin({
manifest: require('./dll/vendor.manifest.json'),
}),
new AddAssetHtmlPlugin({
filepath: path.resolve(__dirname, `dll/${vendorFile}`),
includeSourcemap: false
}),
...
],
};
DllReferencePlugin的作用是将打包好的dll文件传入构建的代码里面,而AddAssetHtmlPlugin的作用是在生成的html文件中加入dll文件的script引用。网上的例子一般是将dll的文件名直接写死的,但由于在上一步构建dll的时候加入了hash,所以要通过fs读取真实的文件名,再注入到html中。
HappyPack
大家都知道webpack是运行在node环境中,而node是单线程的。webpack的打包过程是io密集和计算密集型的操作,如果能fork多个进程并行处理各个任务,将会有效的算短构建时间,HappyPack就能做到这点。下面是它的相关配置:
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
include: [
path.resolve(__dirname, 'src')
],
use: [{
loader: 'happypack/loader?id=happyBabel',
}],
},
{
test: /\.css$/,
include: [
path.resolve(__dirname, 'src')
],
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['happypack/loader?id=happyCss'],
}),
}
],
...
plugins: [
...
new HappyPack({
id: 'happyBabel',
loaders: [{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: ['react', 'es2015', 'stage-0'],
plugins: ['add-module-exports', 'transform-decorators-legacy'],
},
}],
threadPool: happyThreadPool,
verbose: true,
}),
new HappyPack({
id: 'happyCss',
loaders: ['css-loader', 'postcss-loader'],
threadPool: happyThreadPool,
verbose: true,
}),
],
其中happyThreadPool
是根据cpu数量生成的共享进程池,防止过多的占用系统资源。
减少页面加载时间
对于web应用来说,减少页面加载时间一般有2种方法。一是充分利用浏览器缓存,减少网络传输的时间。另外就是减少JS运行的时间,通过SSR等方式实现。利用webpack能有效的抽取出共享的资源,提高缓存的命中率。这里用到的插件除了上文提到的DllPlugin外,还有CommonsChunkPlugin,相关配置如下:
module.exports = {
entry: {
vendor: ['zent','lodash']
app: ['babel-polyfill', 'react-hot-loader/patch', './src/main.js']
},
...
plugins: [
...
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor'],
minChunks: Infinity,
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
minChunks: 3,
children: true,
async: 'chunk-vendor',
}),
new webpack.optimize.CommonsChunkPlugin({
names: ['manifest'],
minChunks: Infinity,
}),
new webpack.HashedModuleIdsPlugin(),
new InlineManifestWebpackPlugin({
name: 'webpackManifest',
}),
...
],
};
插件的第一部分是将vendor构建一个独立包;第二部分是抽取app入口文件code split之后所有子模块的公共模块,进一步减少子模块的大小;第三部分将webpack的启动代码独立打成一个manifest包,配合HashedModuleIdsPlugin可以保证vendor的hash不变。InlineManifestWebpackPlugin的作用是将manifest文件内联到html模板中,减少一次网络请求。
总结
经过上述的优化之后,开发模式构建只需要60秒左右;生产模式构建只需要150秒左右,时间减少一半!缓存命中方面,可以做到基础模块(React等)和比较少变动的模块(组件库)分离出来,当组件库更新的时候依然可以使用基础模块的缓存(通过dll实现)。
通过这次的优化,对webpack的理解加深了不少,取得了比较不错的优化效果。另外也学习了loader和plugin的工作原理,有机会另写一篇文章分享。