欢迎关注我的公众号睿Talk
,获取我最新的文章:
一、前言
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,具体的配置如下:
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 的工作原理,有机会另写一篇文章分享。
如果你对 webpack 的工作原理感兴趣,可以看看我写的另一篇文章webpack启动代码源码解读
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。