前面的話
Webpack升級4之後,小柒踩過的很多坑,這篇文章總結Webpack4的一些新特性,以及常見的優化方式。
Webpack4 新特性
-
不再強制需要
webpack.config.js
配置文件。默認入口爲./src/index.js
,默認輸出./dist目錄
,輸出文件main.js
-
Webpack不再能單獨使用,4.x版本的很多命令都移動到了
webpack-cli
中。所以必須安裝webpack
與webpack-cli
。 -
開箱即用 WebAssembly,webpack4提供了wasm的支持,現在可以引入和導出任何一個 Webassembly 的模塊,也可以寫一個loader來引入C++、C和Rust。(注:WebAssembly 模塊只能在異步chunks中使用)
-
增加模式區分,在配置文件中使用
mode
選項來配置相應的模式:
development
:開發模式。打包默認不壓縮代碼,默認開啓代碼調試
production
:生產模式,線上使用。打包壓縮代碼,不開啓代碼調試 -
一些插件由
optimeztion
配置代替。
比如:CommonsChunkPlugin
廢棄,由optimization.splitChunks
和optimization.runtimeChunk
代替,前者拆分代碼,後者提取runtime代碼- optimize.UglifyJsPlugin 廢棄,由 optimization.minimize 替代,生產環境默認開啓。
-
升級到 webpack4 後,mini-css-extract-plugin 替代 extract-text-webpack-plugin 成爲css分離首選。
量化
speed-measure-webpack-paligin
插件可以測量各個插件和loader所花費的時間。
使用如下:
const SpeedMeasurePlugin = require('speed-measure-webpack-palugin');
const smp = new SpeedMeasurePlugin();
const config = {
// ...webpack配置
}
module.exports = smp.wrap(config);
從打包時間上優化
從哪幾個方面優化
- 減少搜索依賴的時間
- 減少loader解析時間
- 減少js文件壓縮時間: 將所有解析完的代碼,打包到一個文件中,爲了減小白屏時間,webpack爲對其進行優化。js壓縮是發佈編譯的最後階段,通常webpack需要卡好一會,因爲壓縮js代碼要將代碼解析成AST,再根據複雜的規則去分析和處理AST,最後將AST還原爲js。
- 減少二次打包時間:當我們更改項目中的文件是,就要重新打包。有些文件其實不需要再次打包,所以要減少二次打包時間。
1、減少搜索依賴的時間
- 優化loader配置
由於loader轉換文件比較耗時,所以讓儘可能少的文件被loader處理。使用test
、include
、exclude
三個配置項來命中loader要應用的文件。 - 優化resolve.modules配置
resolve.modules用於配置Webpack去哪些目錄下尋找第三方模塊。resolve.modules的默認值是['node_modules']
,意思是先從當前目錄下的./node_modules目錄下去找我們想要的模塊,如果沒有就去上級目錄…/node_modules,再沒有就去…/…/node_modules中找,以此類推。 - 優化resolve.mainFields配置
用於配置第三方模板使用哪個入口文件。 - 優化resolve.alias配置
通過別名來把原來的導入路徑映射成爲一個新的路徑,減少解析時間 - 優化reslove.extensions配置
在導入語句沒有文件後綴時,Webpack會根據resolve.extension自動帶上後綴後去嘗試詢問文件是否存在。 - 優化module.noParse配置
可以讓Webpack忽略對部分模塊沒有采用模塊化的文件的遞歸解析處理。比如:jQuery、ChartJS它們龐大有沒有采用模塊化標準,讓Webpack去解析這些文件耗時又沒有意義。
詳細配置:根據你的項目去選擇
// 編譯代碼的基礎配置
module.exports = {
// ...
module: {
// 項目中使用的 jquery 並沒有採用模塊化標準,webpack 忽略它
noParse: /jquery/,
rules: [
{
// 這裏編譯 js、jsx
// 注意:如果項目源碼中沒有 jsx 文件就不要寫 /\.jsx?$/,提升正則表達式性能
test: /\.(js|jsx)$/,
// babel-loader 支持緩存轉換出的結果,通過 cacheDirectory 選項開啓
use: ['babel-loader?cacheDirectory'],
// 排除 node_modules 目錄下的文件
// node_modules 目錄下的文件都是採用的 ES5 語法,沒必要再通過 Babel 去轉換
exclude: /node_modules/,
},
]
},
resolve: {
// 設置模塊導入規則,import/require時會直接在這些目錄找文件
// 可以指明存放第三方模塊的絕對路徑,以減少尋找
modules: [
path.resolve(`${project}/client/components`),
path.resolve('h5_commonr/components'),
'node_modules'
],
// import導入時省略後綴
// 注意:儘可能的減少後綴嘗試的可能性
extensions: ['.js', '.jsx', '.react.js', '.css', '.json'],
// import導入時別名,減少耗時的遞歸解析操作
alias: {
'@compontents': path.resolve(`${project}/compontents`),
}
},
};
2、 減少loader的解析時間(開啓多進程)
webpack是單線程模式,只能一個一個文件去處理,當打包文件比較大時,打包時間就會比較長。
-
HappyPack(Webpack 3)
原理:將loader的解析交給多個進程並行去處理,發揮CPU多核的能力,從而減少構建時間。
安裝依賴:npm install happypack -D
使用如下:// 比如對js文件的處理: // 引入happypack const HappyPack = require('happypack'); // 引入系統操作模塊 const os = require('os'); // 構造共享進程池,根據系統內核數量,指定進程池的個數,也可以是其他數量 const happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length}); const createHappyPlugin = (id, loaders) => new HappyPack({ id: id, // 唯一標識 loaders: loaders, // 使用的loader // 其他選項 threadPool: happyThreadPool, //使用共享進程池中的子進程去處理任務 verbose: true // 是否允許HappyPack輸出日誌,默認爲true }) module.exports = { // ... 其他選項 module: { rules: [ // ... 其他文件loader配置 { test: /\.js$/, // 對js文件的處理交給`id`爲babel的HappyPack實例 use: ['happypack/loader?id=babel'], // node_modules目錄下的文件都是es5語法,不用通過babel轉換 exclude: /node_modules/, }, ] }, plugins: [ // ...其他插件 createHappyPlugin('babel',[{ loader: 'babel-loader', options: { babelrc: true, cacheDirectory: true // 啓用緩存 } }]) ] }
注意:只在解析時間長的loader上使用,項目小也不必使用。並且happypack不推薦使用,現在已經不再維護它
-
thread-loader(Webpack4 推薦)
使用比較簡單,將這個thread-loader放在其他loader之前就可以了。
原理: 也是開啓多進程來並行loader的解析,被放置了thread-loader的loader就會在單獨的worker池中運行,一個worker就是一個node進程。每個單獨的進程處理時間限制在600ms。
安裝依賴:
npm install thread-loader -D
使用如下:
module.exports = { // ... module: { rules: [ { test: /\.js$/, exclude: /node_modules/, // 創建一個 js worker 池 use: [ 'thread-loader', 'babel-loader' ] }, { test: /\.s?css$/, exclude: /node_modules/, // 創建一個 css worker 池 use: [ 'style-loader', 'thread-loader', { loader: 'css-loader', options: { modules: true, localIdentName: '[name]__[local]--[hash:base64:5]', importLoaders: 1 } }, ] } // ... ] // ...
3、減少js壓縮時間
發佈到到線上的代碼,一般都會壓縮js代碼。
-
UglifyJsPlugin (Webpack 3)
UglifyJsPlugin是webpack3內置的的插件,使用時引入就好,是單進程的。
原理: 配置在壓縮過程中使用的規則。
使用如下,給出最優化的代碼配置:
const UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); module.exports = { // ... plugins: [ new UglifyJsPlugin({ compress: { warnings: false,// 在UglifyJsPlugin刪除沒有用到的代碼時,不是輸出警告 drop_console: true,// 刪除所有的'console'語句 collapse_var: true,// 內嵌已定義,但是隻用了一次的變量 reduce_var: true,// 提取出現了多次但沒有定義成變量去引用的靜態值。 }, output:{ // 最緊湊的輸出 beautify: false, // 刪除所有的註釋 comments: false } }) ]
此外,Webpack提供了更簡單的方法來接入UglifyJsPlugin,直接在啓動Webpack時,帶上
--optimize-minimize
參數。即webpack --optimize-minimize
。 -
webpack-parallel-uglify-plugin (Webpack 3)
看名字都知道,並行的UglifyPlugin嘛,就是利用了多進程。
原理:開啓多個子進程,對多個文件的壓縮工作分配給多個子進程去完成,每個子進程其實還是通過UgilifyJS去壓縮代碼,但是並行執行。子進程處理完後再把結果發送給主進程。
安裝依賴:
npm install webpack-parallel-uglify-plugin -D
使用如下:
const ParallelUglifyJsPlugin = require('webpack-parallel-uglify-plugin'); module.exports = { // ... plugins: [ new ParallelUglifyJsPlugin({ cacheDir: './cache' // 緩存壓縮後的結果 // 傳遞給UglifyJS的參數 uglifyJS: { compress: { warnings: false,// 在UglifyJsPlugin刪除沒有用到的代碼時,不是輸出警告 drop_console: true,// 刪除所有的'console'語句 collapse_var: true,// 內嵌已定義,但是隻用了一次的變量 reduce_var: true,// 提取出現了多次但沒有定義成變量去引用的靜態值。 }, output:{ // 最緊湊的輸出 beautify: false, // 刪除所有的註釋 comments: false } } }) ]
上面的
ugilifyJS
參數是用於壓縮ES5代碼是時的配置,也可以是ugilifyES
:用於壓縮ES6代碼。 -
terser-webpack-plugin(Webpack 4)
terser:簡要的。
官方定義:用於ES6+的js解析器、壓縮工具。爲何選擇terser:不再維護UglifyES,並且UgilifyJS不支持ES6+。terser是UglifyES的一個分支,主要保留了與UglifyJS和UglifyES的API和CLI兼容性。
原理:開啓多進程
安裝依賴:
npm install terser-webpack-plugin -D
使用如下:module.exports = { optimization: { minimizer: [ new TerserPlugin({ parallel: true, }), ], }, };
4、 減少二次打包時間
合理利用緩存,來減少二次打包時間。比如cache-loader
HardSourceWebpackPlugin
、 babel-loader的cacheDirectory標誌
。但所有的緩存方法都有啓動開銷。二次打包節約時間,初次打包很慢
-
cache-loader
cache-loader的配置很簡單,只要放在其他loader之前即可。如果只是下
babel-loader
配置cache,使用babel-loader
的cacheDirectory
安裝依賴:
npm install cache-loader -D
module.exports = { module: { rules: [ { test: /\.ext$/, use: ['cache-loader', ...loaders], include: path.resolve('src'), }, ], }, };
-
hard-source-webpack-plugin
爲模塊提供中間緩存。緩存默認的存放路徑是:
node_modules/.cache/hard-source
.使用如下:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); // ... plugins: [ new HardSourceWebpackPlugin() ],
非首次使用
HardSourceWebpackPlugin
打包的時間,明顯比沒有使用要快:
從打包體積上優化
-
Tree Shaking (依賴需要採用ES6模塊化語法的代碼)
可以去除無用代碼。
要求:必須使用ES6模塊化語法,使用babel時,要關閉其模塊轉換功能,修改.babelrc文件或者在 webpack.config.js 配置文件中將modules設置爲false。
// .babelrc { "presets": [ ["env", { "modules": false // 保留es6的模塊化語法 } ] ] }
Webpack4下的配置:
- 首先,你必須處於生產模式。Webpack 只有在壓縮代碼的時候會 tree-shaking。
- 其次,開啓優化選項 “usedExports” 。這樣Webpack就看可以提示你哪些是用不上的代碼
- 最後,你需要使用一個支持刪除死代碼的壓縮器。上面提到過terser-webpack-plugin
注意:生產環境下默認開啓 。
const config = { mode: 'production', // 生產模式 optimization: { usedExports: true, // minimizer: [ new TerserPlugin({...}) ] } };
還需注意的一點:
在
package.json
文件中,有一個特殊的屬性sideEffects
,它有三個可能值:-
true
: 是默認值,意味着所有的文件都不能進行tree-shaking
-
false
: 表示所有的文件都可以進行tree-shaking
-
[...]
: 表示在數組中的文件,是不可以進行tree-shaking
的,其他文件都可以.// 所有文件都有副作用,全都不可 tree-shaking { "sideEffects": true } // 沒有文件有副作用,全都可以 tree-shaking { "sideEffects": false } // 只有這些文件有副作用,所有其他文件都可以 tree-shaking,但會保留這些文件 { "sideEffects": [ "./src/file1.js", "./src/file2.js" ] }
-
Scope Hoisting (依賴採用ES6模塊化語法的代碼)
可以讓Webpack 打包出來的代碼更小。
原理:分析模塊之間的依賴關係,儘可能將被打散的模塊合併到一個函數中,但前提是不能造成代碼冗餘。因此只有那些被引用了一次的模塊才能被合併。
使用如下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); module.exports = { // ... resolve: { // 針對Npm中的第三方模塊優先採用jsnext:main中指向ES6模塊化語法的文件 mainFields:['jsnext:main', 'browser', 'main'] }, plugins: [ // 開啓Scope Hoisting new ModuleConcatenationPlugin() ], // ...
注意:生產環境下默認開啓。
最後
還有使用DllPlugin
與DllReferencePlugin
分離基礎模塊(vue-router、vuex等)、使用optimization.splitChunks
與optimization.runtimeChunk
分離公共代碼。這兩部分內容放到下篇總結。
參考文章: