前言
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的工作原理,有機會另寫一篇文章分享。