一、前言
webpack 的出现为前端开发带来翻天覆地的变化,无论你是用 React,Vue 还是 Angular,webpack 都是主流的构建工具。我们每天都跟它打交道,但却很少主动去了解它,就像写字楼里的礼仪小姐姐,既熟悉又陌生。随着项目复杂度的上升,打包构建的时间会越来越长。终于有一天,你发现npm run dev
后,去泡个茶,上了个厕所,跟同事 bb 一轮后回到座位,项目还没构建完的时候,你就会下定决心好好了解下这个熟悉的陌生人。
这次优化的目标主要有两个:
- 加快编译构建速度
- 减少页面加载的时间
现状是每次开发模式构建,大概要花 120 秒;生产模式构建,大概要花 300 秒。项目总共有将近 150 个 chunk。
如果你对 webpack 的工作原理感兴趣,可以看看我写的另一篇文章webpack启动代码源码解读
二、加快编译构建速度
有 2 种方式可以加快编译的速度,分别是减少每次打包的文件数目,和并行的去执行打包任务。这里用到了 2 个 webpack 插件:
- DllPlugin(减少每次打包的文件数目)
- HappyPack(并行的去执行打包任务)
下面对这两个插件作详细的介绍。
- DllPlugin
dll 是 Dynamic Link Library(动态链接库)的缩写,是 Windows 系统共享函数库的一种方式。将一些比较少改变的库和工具,比如 React、React-DOM,事先独立打包成一个 chunk,以后每次构建的时候再直接导入,就不用每次都对这些文件打包了。这里有 2 个分解动作:
- 独立打包 dll
- 导入 dll
使用 DllPlugin 可以独立打包 dll,具体的配置如下:
代码语言:javascript复制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,配置如下
代码语言:javascript复制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 就能做到这点。下面是它的相关配置:
代码语言:javascript复制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,相关配置如下:
代码语言:javascript复制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 的工作原理,有机会另写一篇文章分享。
如果你对 webpack 的工作原理感兴趣,可以看看我写的另一篇文章webpack启动代码源码解读