帶你深度解鎖Webpack系列(基礎篇)和帶你深度解鎖Webpack系列(進階篇),主要是講解了 Webpack
的配置,可是隨着項目愈來愈大,構建速度可能會愈來愈慢,構建出來的js的體積也愈來愈大,此時就須要對配置進行優化。css
文中羅列出了十多種優化方式,你們能夠結合本身的項目,選擇適當的方式進行優化。這些 Webpack
插件的源碼我大多也沒有看過,主要是結合 Webpack
官方文檔以及項目實踐,在驗證後輸出了本文,若是文中有錯誤的地方,歡迎在評論區指正。html
鑑於前端技術變動迅速,祭出本篇文章基於 Webpack
的版本號:前端
├── webpack@4.41.5 └── webpack-cli@3.3.10
本文對應的項目地址(編寫本文時使用)供參考:https://github.com/YvetteLau/...node
有時,咱們覺得的優化是負優化,這時,若是有一個量化的指標能夠看出先後對比,那將會是再好不過的一件事。react
speed-measure-webpack-plugin
插件能夠測量各個插件和loader
所花費的時間,使用以後,構建時,會獲得相似下面這樣的信息:jquery
對比先後的信息,來肯定優化的效果。webpack
speed-measure-webpack-plugin 的使用很簡單,能夠直接用其來包裹 Webpack
的配置:git
//webpack.config.js const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); const smp = new SpeedMeasurePlugin(); const config = { //...webpack配置 } module.exports = smp.wrap(config);
咱們能夠經過 exclude
、include
配置來確保轉譯儘量少的文件。顧名思義,exclude
指定要排除的文件,include
指定要包含的文件。github
exclude
的優先級高於 include
,在 include
和 exclude
中使用絕對路徑數組,儘可能避免 exclude
,更傾向於使用 include
。web
//webpack.config.js const path = require('path'); module.exports = { //... module: { rules: [ { test: /\.js[x]?$/, use: ['babel-loader'], include: [path.resolve(__dirname, 'src')] } ] }, }
下圖是我未配置 include
和配置了 include
的構建結果對比:
在一些性能開銷較大的 loader
以前添加 cache-loader
,將結果緩存中磁盤中。默認保存在 node_modueles/.cache/cache-loader
目錄下。
首先安裝依賴:
npm install cache-loader -D
cache-loader
的配置很簡單,放在其餘 loader
以前便可。修改Webpack
的配置以下:
module.exports = { //... module: { //個人項目中,babel-loader耗時比較長,因此我給它配置了`cache-loader` rules: [ { test: /\.jsx?$/, use: ['cache-loader','babel-loader'] } ] } }
若是你跟我同樣,只打算給 babel-loader
配置 cache
的話,也能夠不使用 cache-loader
,給 babel-loader
增長選項 cacheDirectory
。
cacheDirectory
:默認值爲 false
。當有設置時,指定的目錄將用來緩存 loader
的執行結果。以後的 Webpack
構建,將會嘗試讀取緩存,來避免在每次執行時,可能產生的、高性能消耗的 Babel
從新編譯過程。設置空值或者 true
的話,使用默認緩存目錄:node_modules/.cache/babel-loader
。開啓 babel-loader
的緩存和配置 cache-loader
,我比對了下,構建時間很接近。
因爲有大量文件須要解析和處理,構建是文件讀寫和計算密集型的操做,特別是當文件數量變多後,Webpack
構建慢的問題會顯得嚴重。文件讀寫和計算操做是沒法避免的,那能不能讓 Webpack
同一時刻處理多個任務,發揮多核 CPU 電腦的威力,以提高構建速度呢?
HappyPack
就能讓 Webpack
作到這點,它把任務分解給多個子進程去併發的執行,子進程處理完後再把結果發送給主進程。
首先須要安裝 happypack
:
npm install happypack -D
修改配置文件:
const Happypack = require('happypack'); module.exports = { //... module: { rules: [ { test: /\.js[x]?$/, use: 'Happypack/loader?id=js', include: [path.resolve(__dirname, 'src')] }, { test: /\.css$/, use: 'Happypack/loader?id=css', include: [ path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist') ] }, { test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2|.gexf)$/, use: 'Happypack/loader?id=file', include: [ path.resolve(__dirname, 'src'), path.resolve(__dirname, 'public'), path.resolve(__dirname, 'node_modules', 'bootstrap', 'dist') ] } ] }, plugins: [ new Happypack({ id: 'js', //和rule中的id=js對應 //將以前 rule 中的 loader 在此配置 use: ['babel-loader'] //必須是數組 }), new Happypack({ id: 'css',//和rule中的id=css對應 use: ['style-loader', 'css-loader','postcss-loader'], }), new Happypack({ id: 'file', //和rule中的id=file對應 use: [{ loader: 'url-loader', options: { limit: 10240 //10K } }], }), ] }
happypack
默認開啓 CPU核數 - 1
個進程,固然,咱們也能夠傳遞 threads
給 Happypack
。
說明:當 postcss-loader
配置在 Happypack
中,必需要在項目中建立 postcss.config.js
。
//postcss.config.js module.exports = { plugins: [ require('autoprefixer')() ] }
不然,會拋出錯誤: Error: No PostCSS Config found
另外,當你的項目不是很複雜時,不須要配置 happypack
,由於進程的分配和管理也須要時間,並不能有效提高構建速度,甚至會變慢。
除了使用 Happypack
外,咱們也可使用 thread-loader
,把 thread-loader
放置在其它 loader
以前,那麼放置在這個 loader
以後的 loader
就會在一個單獨的 worker
池中運行。
在 worker 池(worker pool)中運行的 loader 是受到限制的。例如:
loader
不能產生新的文件。loader
不能使用定製的 loader
API(也就是說,經過插件)。loader
沒法獲取 webpack
的選項設置。首先安裝依賴:
npm install thread-loader -D
修改配置:
module.exports = { module: { //個人項目中,babel-loader耗時比較長,因此我給它配置 thread-loader rules: [ { test: /\.jsx?$/, use: ['thread-loader', 'cache-loader', 'babel-loader'] } ] } }
thread-loader
和 Happypack
我對比了一下,構建時間基本沒什麼差異。不過 thread-loader
配置起來爲簡單。
雖然不少 webpack
優化的文章上會說起多進程壓縮的優化,不論是 webpack-parallel-uglify-plugin
或者是 uglifyjs-webpack-plugin
配置 parallel
。不過這裏我要說一句,不必單獨安裝這些插件,它們並不會讓你的 Webpack
更快。
由於當前 Webpack
默認使用的是 TerserWebpackPlugin
,默認開啓了多進程和緩存,構建時,你的項目中能夠看到 terser
的緩存文件 node_modules/.cache/terser-webpack-plugin
。
HardSourceWebpackPlugin
爲模塊提供中間緩存,緩存默認的存放路徑是: node_modules/.cache/hard-source
。
配置 hard-source-webpack-plugin
,首次構建時間沒有太大變化,可是第二次開始,構建時間大約能夠節約 80%。
首先安裝依賴:
npm install hard-source-webpack-plugin -D
修改 webpack
的配置:
//webpack.config.js var HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); module.exports = { //... plugins: [ new HardSourceWebpackPlugin() ] }
用另一個比較大的項目測試了下,配置了 HardSourceWebpackPlugin
,構建時間從 8S 左右降到了 2S 左右。
HardSourceWebpackPlugin文檔中 列出了一些你可能會遇到的問題以及如何解決,例如熱更新失效,或者某些配置不生效等。
若是一些第三方模塊沒有AMD/CommonJS規範版本,可使用 noParse
來標識這個模塊,這樣 webpack
會引入這些模塊,可是不進行轉化和解析,從而提高 webpack
的構建性能 ,例如:jquery
、lodash
。
noParse 屬性的值是一個正則表達式或者是一個 function
。
//webpack.config.js module.exports = { //... module: { noParse: /jquery|lodash/ } }
我當前的 webpack-optimize
項目中,沒有使用 jquery
或者是 lodash
。
所以新建一個項目測試,只引入 jquery
和 loadsh
,而後配置 noParse
和不配置 noParse
,分別構建比對時間。
配置noParse
前,構建須要 2392ms
。配置了 noParse
以後,構建須要 1613ms
。 若是你使用到了不須要解析的第三方依賴,那麼配置 noParse
很顯然是必定會起到優化做用的。
resolve
配置 webpack
如何尋找模塊所對應的文件。假設咱們肯定模塊都從根目錄下的 node_modules
中查找,咱們能夠配置:
//webpack.config.js const path = require('path'); module.exports = { //... resolve: { modules: [path.resolve(__dirname, 'node_modules')], } }
須要記住的是,若是你配置了上述的 resolve.moudles
,可能會出現問題,例如,你的依賴中還存在 node_modules
目錄,那麼就會出現,對應的文件明明在,可是卻提示找不到。所以呢,我的不推薦配置這個。若是其餘同事不熟悉這個配置,遇到這個問題時,會摸不着頭腦。
另外,resolve
的 extensions
配置,默認是 ['.js', '.json']
,若是你要對它進行配置,記住將頻率最高的後綴放在第一位,而且控制列表的長度,以減小嚐試次數。
本項目較小,所以測試時,此處優化效果不明顯。
webpack
的內置插件,做用是忽略第三方包指定目錄。
例如: moment
(2.24.0版本) 會將全部本地化內容和核心功能一塊兒打包,咱們就可使用 IgnorePlugin
在打包時忽略本地化內容。
//webpack.config.js module.exports = { //... plugins: [ //忽略 moment 下的 ./locale 目錄 new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/) ] }
在使用的時候,若是咱們須要指定語言,那麼須要咱們手動的去引入語言包,例如,引入中文語言包:
import moment from 'moment'; import 'moment/locale/zh-cn';// 手動引入
index.js
中只引入 moment
,打包出來的 bundle.js
大小爲 263KB
,若是配置了 IgnorePlugin
,單獨引入 moment/locale/zh-cn
,構建出來的包大小爲 55KB
。
咱們能夠將一些JS文件存儲在 CDN
上(減小 Webpack
打包出來的 js
體積),在 index.html
中經過 <script>
標籤引入,如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="root">root</div> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> </body> </html>
咱們但願在使用時,仍然能夠經過 import
的方式去引用(如 import $ from 'jquery'
),而且但願 webpack
不會對其進行打包,此時就能夠配置 externals
。
//webpack.config.js module.exports = { //... externals: { //jquery經過script引入以後,全局中即有了 jQuery 變量 'jquery': 'jQuery' } }
有些時候,若是全部的JS文件都打成一個JS文件,會致使最終生成的JS文件很大,這個時候,咱們就要考慮拆分 bundles
。
DllPlugin
和 DLLReferencePlugin
能夠實現拆分 bundles
,而且能夠大大提高構建速度,DllPlugin
和 DLLReferencePlugin
都是 webpack
的內置模塊。
咱們使用 DllPlugin
將不會頻繁更新的庫進行編譯,當這些依賴的版本沒有變化時,就不須要從新編譯。咱們新建一個 webpack
的配置文件,來專門用於編譯動態連接庫,例如名爲: webpack.config.dll.js
,這裏咱們將 react
和 react-dom
單獨打包成一個動態連接庫。
//webpack.config.dll.js const webpack = require('webpack'); const path = require('path'); module.exports = { entry: { react: ['react', 'react-dom'] }, mode: 'production', output: { filename: '[name].dll.[hash:6].js', path: path.resolve(__dirname, 'dist', 'dll'), library: '[name]_dll' //暴露給外部使用 //libraryTarget 指定如何暴露內容,缺省時就是 var }, plugins: [ new webpack.DllPlugin({ //name和library一致 name: '[name]_dll', path: path.resolve(__dirname, 'dist', 'dll', 'manifest.json') //manifest.json的生成路徑 }) ] }
在 package.json
的 scripts
中增長:
{ "scripts": { "dev": "NODE_ENV=development webpack-dev-server", "build": "NODE_ENV=production webpack", "build:dll": "webpack --config webpack.config.dll.js" }, }
執行 npm run build:all
,能夠看到 dist
目錄以下,之因此將動態連接庫單獨放在 dll
目錄下,主要是爲了使用 CleanWebpackPlugin
更爲方便的過濾掉動態連接庫。
dist └── dll ├── manifest.json └── react.dll.9dcd9d.js
manifest.json
用於讓 DLLReferencePlugin
映射到相關依賴上。
修改 webpack
的主配置文件: webpack.config.js
的配置:
//webpack.config.js const webpack = require('webpack'); const path = require('path'); module.exports = { //... devServer: { contentBase: path.resolve(__dirname, 'dist') }, plugins: [ new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, 'dist', 'dll', 'manifest.json') }), new CleanWebpackPlugin({ cleanOnceBeforeBuildPatterns: ['**/*', '!dll', '!dll/**'] //不刪除dll目錄 }), //... ] }
使用 npm run build
構建,能夠看到 bundle.js
的體積大大減小。
修改 public/index.html
文件,在其中引入 react.dll.js
<script src="/dll/react.dll.9dcd9d.js"></script>
構建速度
包體積
抽離公共代碼是對於多頁應用來講的,若是多個頁面引入了一些公共模塊,那麼能夠把這些公共的模塊抽離出來,單獨打包。公共代碼只須要下載一次就緩存起來了,避免了重複下載。
抽離公共代碼對於單頁應用和多頁應該在配置上沒有什麼區別,都是配置在 optimization.splitChunks
中。
//webpack.config.js module.exports = { optimization: { splitChunks: {//分割代碼塊 cacheGroups: { vendor: { //第三方依賴 priority: 1, //設置優先級,首先抽離第三方模塊 name: 'vendor', test: /node_modules/, chunks: 'initial', minSize: 0, minChunks: 1 //最少引入了1次 }, //緩存組 common: { //公共模塊 chunks: 'initial', name: 'common', minSize: 100, //大小超過100個字節 minChunks: 3 //最少引入了3次 } } } } }
即便是單頁應用,一樣可使用這個配置,例如,打包出來的 bundle.js 體積過大,咱們能夠將一些依賴打包成動態連接庫,而後將剩下的第三方依賴拆出來。這樣能夠有效減少 bundle.js 的體積大小。固然,你還能夠繼續提取業務代碼的公共模塊,此處,由於我項目中源碼較少,因此沒有配置。
runtimeChunk
runtimeChunk
的做用是將包含 chunk
映射關係的列表從 main.js
中抽離出來,在配置了 splitChunk
時,記得配置 runtimeChunk
.
module.exports = { //... optimization: { runtimeChunk: { name: 'manifest' } } }
最終構建出來的文件中會生成一個 manifest.js
。
在作 webpack
構建優化的時候,vendor
打出來超過了1M,react
和 react-dom
已經打包成了DLL。
所以須要藉助 webpack-bundle-analyzer
查看一下是哪些包的體積較大。
首先安裝依賴:
npm install webpack-bundle-analyzer -D
使用也很簡單,修改下咱們的配置:
//webpack.config.prod.js const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const merge = require('webpack-merge'); const baseWebpackConfig = require('./webpack.config.base'); module.exports = merge(baseWebpackConfig, { //.... plugins: [ //... new BundleAnalyzerPlugin(), ] })
npm run build
構建,會默認打開: http://127.0.0.1:8888/
,能夠看到各個包的體積:
進一步對 vendor
進行拆分,將 vendor
拆分紅了4個(使用 splitChunks
進行拆分便可)。
module.exports = { optimization: { concatenateModules: false, splitChunks: {//分割代碼塊 maxInitialRequests:6, //默認是5 cacheGroups: { vendor: { //第三方依賴 priority: 1, name: 'vendor', test: /node_modules/, chunks: 'initial', minSize: 100, minChunks: 1 //重複引入了幾回 }, 'lottie-web': { name: "lottie-web", // 單獨將 react-lottie 拆包 priority: 5, // 權重需大於`vendor` test: /[\/]node_modules[\/]lottie-web[\/]/, chunks: 'initial', minSize: 100, minChunks: 1 //重複引入了幾回 }, //... } }, }, }
從新構建,結果以下所示:
若是使用ES6的import
語法,那麼在生產環境下,會自動移除沒有使用到的代碼。
//math.js const add = (a, b) => { console.log('aaaaaa') return a + b; } const minus = (a, b) => { console.log('bbbbbb') return a - b; } export { add, minus }
//index.js import {add, minus} from './math'; add(2,3);
構建的最終代碼裏,minus
函數不會被打包進去。
變量提高,能夠減小一些變量聲明。在生產環境下,默認開啓。
另外,你們測試的時候注意一下,speed-measure-webpack-plugin
和 HotModuleReplacementPlugin
不能同時使用,不然會報錯:
webpack
的配置部分到此基本就結束了,typescript
、eslint
和 prettier
,這裏沒有說起,你們能夠本身配置一下,也能夠參考我以前練手時配置的一個項目:https://github.com/YvetteLau/...
若是你對 babel
還不太熟悉的話,那麼能夠閱讀這篇文章:不容錯過的 Babel7 知識。
在不配置 @babel/plugin-transform-runtime
時,babel
會使用很小的輔助函數來實現相似 _createClass
等公共方法。默認狀況下,它將被注入(inject
)到須要它的每一個文件中。可是這樣的結果就是致使構建出來的JS體積變大。
咱們也並不須要在每一個 js
中注入輔助函數,所以咱們可使用 @babel/plugin-transform-runtime
,@babel/plugin-transform-runtime
是一個能夠重複使用 Babel
注入的幫助程序,以節省代碼大小的插件。
所以咱們能夠在 .babelrc
中增長 @babel/plugin-transform-runtime
的配置。
{ "presets": [], "plugins": [ [ "@babel/plugin-transform-runtime" ] ] }
以上就是我目前爲止使用到的一些優化,若是你有更好的優化方式,歡迎在評論區留言,感謝閱讀。
若是本文對你有幫助的話,給本文點個贊吧。
參考文檔: