查看全部文檔頁面: 全棧開發,獲取更多信息。原文連接:webpack優化,原文廣告模態框遮擋,閱讀體驗很差,因此整理成本文,方便查找。css
優化構建速度。在項目龐大時構建耗時可能會變的很長,每次等待構建的耗時加起來也會是個大數目。html
優化使用體驗。經過自動化手段完成一些重複的工做,讓咱們專一於解決問題自己。前端
優化輸出質量的目的是爲了給用戶呈現體驗更好的網頁,例如減小首屏加載時間、提高性能流暢度等。 這相當重要,由於在互聯網行業競爭日益激烈的今天,這可能關係到你的產品的生死。node
優化輸出質量本質是優化構建輸出的要發佈到線上的代碼,分爲如下幾點:react
減小用戶能感知到的加載時間,也就是首屏加載時間。webpack
提高流暢度,也就是提高代碼性能。git
Webpack 啓動後會從配置的 Entry 出發,解析出文件中的導入語句,再遞歸的解析。 在遇到導入語句時 Webpack 會作兩件事情:github
require('react')
導入語句對應的文件是 ./node_modules/react/react.js,require('./util')
對應的文件是 ./util.js
。babel-loader
去處理。loader
配置因爲 Loader 對文件的轉換操做很耗時,須要讓儘量少的文件被 Loader 處理。web
在 Module 中介紹過在使用 Loader 時能夠經過 test
、 include
、 exclude
三個配置項來命中 Loader 要應用規則的文件。 爲了儘量少的讓文件被 Loader 處理,能夠經過 include
去命中只有哪些文件須要被處理。正則表達式
以採用 ES6 的項目爲例,在配置 babel-loader
時,能夠這樣:
module.exports = { module: { rules: [ { // 若是項目源碼中只有 js 文件就不要寫成 /\.jsx?$/,提高正則表達式性能 test: /\.js$/, // babel-loader 支持緩存轉換出的結果,經過 cacheDirectory 選項開啓 use: ['babel-loader?cacheDirectory'], // 只對項目根目錄下的 src 目錄中的文件採用 babel-loader include: path.resolve(__dirname, 'src'), }, ] }, };
你能夠適當的調整項目的目錄結構,以方便在配置 Loader 時經過
include
去縮小命中範圍。
resolve.modules
配置在 Resolve 中介紹過 resolve.modules
用於配置 Webpack 去哪些目錄下尋找第三方模塊。
resolve.modules
的默認值是 ['node_modules']
,含義是先去當前目錄下的 ./node_modules
目錄下去找想找的模塊,若是沒找到就去上一級目錄 ../node_modules
中找,再沒有就去 ../../node_modules
中找,以此類推,這和 Node.js 的模塊尋找機制很類似。
當安裝的第三方模塊都放在項目根目錄下的 ./node_modules
目錄下時,沒有必要按照默認的方式去一層層的尋找,能夠指明存放第三方模塊的絕對路徑,以減小尋找,配置以下:
module.exports = { resolve: { // 使用絕對路徑指明第三方模塊存放的位置,以減小搜索步驟 // 其中 __dirname 表示當前工做目錄,也就是項目根目錄 modules: [path.resolve(__dirname, 'node_modules')] }, };
resolve.mainFields
配置在 Resolve 中介紹過 resolve.mainFields
用於配置第三方模塊使用哪一個入口文件。
安裝的第三方模塊中都會有一個 package.json
文件用於描述這個模塊的屬性,其中有些字段用於描述入口文件在哪裏,resolve.mainFields
用於配置採用哪一個字段做爲入口文件的描述。
能夠存在多個字段描述入口文件的緣由是由於有些模塊能夠同時用在多個環境中,準對不一樣的運行環境須要使用不一樣的代碼。 以 isomorphic-fetch 爲例,它是 fetch API 的一個實現,但可同時用於瀏覽器和 Node.js 環境。 它的 package.json
中就有2個入口文件描述字段:
{ "browser": "fetch-npm-browserify.js", "main": "fetch-npm-node.js" }
isomorphic-fetch
在不一樣的運行環境下使用不一樣的代碼是由於 fetch API 的實現機制不同,在瀏覽器中經過原生的fetch
或者XMLHttpRequest
實現,在 Node.js 中經過http
模塊實現。
resolve.mainFields
的默認值和當前的 target
配置有關係,對應關係以下:
target
爲 web
或者 webworker
時,值是 ["browser", "module", "main"]
target
爲其它狀況時,值是 ["module", "main"]
以 target
等於 web 爲例,Webpack 會先採用第三方模塊中的 browser
字段去尋找模塊的入口文件,若是不存在就採用 module
字段,以此類推。
爲了減小搜索步驟,在你明確第三方模塊的入口文件描述字段時,你能夠把它設置的儘可能少。 因爲大多數第三方模塊都採用 main
字段去描述入口文件的位置,能夠這樣配置 Webpack:
module.exports = { resolve: { // 只採用 main 字段做爲入口文件描述字段,以減小搜索步驟 mainFields: ['main'], }, };
使用本方法優化時,你須要考慮到全部運行時依賴的第三方模塊的入口文件描述字段,就算有一個模塊搞錯了均可能會形成構建出的代碼沒法正常運行。
resolve.alias
配置resolve.alias
配置項經過別名來把原導入路徑映射成一個新的導入路徑。
在實戰項目中常常會依賴一些龐大的第三方模塊,以 React 庫爲例,安裝到 node_modules
目錄下的 React 庫的目錄結構以下:
├── dist │ ├── react.js │ └── react.min.js ├── lib │ ... 還有幾十個文件被忽略 │ ├── LinkedStateMixin.js │ ├── createClass.js │ └── React.js ├── package.json └── react.js
能夠看到發佈出去的 React 庫中包含兩套代碼:
lib
目錄下,以 package.json
中指定的入口文件 react.js
爲模塊的入口。dist/react.js
是用於開發環境,裏面包含檢查和警告的代碼。dist/react.min.js
是用於線上環境,被最小化了。默認狀況下 Webpack 會從入口文件 ./node_modules/react/react.js
開始遞歸的解析和處理依賴的幾十個文件,這會時一個耗時的操做。 經過配置 resolve.alias
可讓 Webpack 在處理 React 庫時,直接使用單獨完整的 react.min.js
文件,從而跳過耗時的遞歸解析操做。
相關 Webpack 配置以下:
module.exports = { resolve: { // 使用 alias 把導入 react 的語句換成直接使用單獨完整的 react.min.js 文件, // 減小耗時的遞歸解析操做 alias: { 'react': path.resolve(__dirname, './node_modules/react/dist/react.min.js'), } }, };
除了 React 庫外,大多數庫發佈到 Npm 倉庫中時都會包含打包好的完整文件,對於這些庫你也能夠對它們配置
alias
。可是對於有些庫使用本優化方法後會影響到後面要講的使用
Tree-Shaking
去除無效代碼的優化,由於打包好的完整文件中有部分代碼你的項目可能永遠用不上。 通常對總體性比較強的庫採用本方法優化,由於完整文件中的代碼是一個總體,每一行都是不可或缺的。 可是對於一些工具類的庫,例如lodash
,你的項目可能只用到了其中幾個工具函數,你就不能使用本方法去優化,由於這會致使你的輸出代碼中包含不少永遠不會執行的代碼。
resolve.extensions
配置在導入語句沒帶文件後綴時,Webpack 會自動帶上後綴後去嘗試詢問文件是否存在。resolve.extensions
用於配置在嘗試過程當中用到的後綴列表,默認是:
extensions: ['.js', '.json']
也就是說當遇到 require('./data')
這樣的導入語句時,Webpack 會先去尋找 ./data.js
文件,若是該文件不存在就去尋找 ./data.json
文件,若是仍是找不到就報錯。
若是這個列表越長,或者正確的後綴在越後面,就會形成嘗試的次數越多,因此 resolve.extensions
的配置也會影響到構建的性能。 在配置 resolve.extensions
時你須要遵照如下幾點,以作到儘量的優化構建性能:
require('./data') 寫成 require('./data.json')
。相關 Webpack 配置以下:
module.exports = { resolve: { // 儘量的減小後綴嘗試的可能性 extensions: ['js'], }, };
module.noParse
配置module.noParse
配置項可讓 Webpack 忽略對部分沒采用模塊化的文件的遞歸解析處理,這樣作的好處是能提升構建性能。 緣由是一些庫,例如 jQuery 、ChartJS, 它們龐大又沒有采用模塊化標準,讓 Webpack 去解析這些文件耗時又沒有意義。
在上面的 優化 resolve.alias
配置 中講到單獨完整的 react.min.js
文件就沒有采用模塊化,讓咱們來經過配置 module.noParse
忽略對 react.min.js
文件的遞歸解析處理, 相關 Webpack 配置以下:
const path = require('path'); module.exports = { module: { // 獨完整的 `react.min.js` 文件就沒有采用模塊化,忽略對 `react.min.js` 文件的遞歸解析處理 noParse: [/react\.min\.js$/], }, };
注意被忽略掉的文件裏不該該包含import
、require
、define
等模塊化語句,否則會致使構建出的代碼中包含沒法在瀏覽器環境下執行的模塊化語句。
以上就是全部和縮小文件搜索範圍相關的構建性能優化了,在根據本身項目的須要去按照以上方法改造後,你的構建速度必定會有所提高。
要給 Web 項目構建接入動態連接庫的思想,須要完成如下事情:
爲何給 Web 項目構建接入動態連接庫的思想後,會大大提高構建速度呢? 緣由在於包含大量複用模塊的動態連接庫只須要編譯一次,在以後的構建過程當中被動態連接庫包含的模塊將不會在從新編譯,而是直接使用動態連接庫中的代碼。 因爲動態連接庫中大多數包含的是經常使用的第三方模塊,例如 react
、react-dom
,只要不升級這些模塊的版本,動態連接庫就不用從新編譯。
Webpack 已經內置了對動態連接庫的支持,須要經過2個內置的插件接入,它們分別是:
下面以基本的 React 項目爲例,爲其接入 DllPlugin,在開始前先來看下最終構建出的目錄結構:
├── main.js ├── polyfill.dll.js ├── polyfill.manifest.json ├── react.dll.js └── react.manifest.json
其中包含兩個動態連接庫文件,分別是:
polyfill.dll.js
裏面包含項目全部依賴的 polyfill
,例如 Promise、fetch 等 API。react.dll.js
裏面包含 React 的基礎運行環境,也就是 react
和 react-dom
模塊。以 react.dll.js
文件爲例,其文件內容大體以下:
var _dll_react = (function(modules) { // ... 此處省略 webpackBootstrap 函數代碼 }([ function(module, exports, __webpack_require__) { // 模塊 ID 爲 0 的模塊對應的代碼 }, function(module, exports, __webpack_require__) { // 模塊 ID 爲 1 的模塊對應的代碼 }, // ... 此處省略剩下的模塊對應的代碼 ]));
可見一個動態連接庫文件中包含了大量模塊的代碼,這些模塊存放在一個數組裏,用數組的索引號做爲 ID。 而且還經過 _dll_react
變量把本身暴露在了全局中,也就是能夠經過 window._dll_react
能夠訪問到它裏面包含的模塊。
其中 polyfill.manifest.json
和 react.manifest.json
文件也是由 DllPlugin 生成出,用於描述動態連接庫文件中包含哪些模塊, 以 react.manifest.json
文件爲例,其文件內容大體以下:
<p data-height="565" data-theme-id="0" data-slug-hash="GdVvmZ" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="react.manifest.json" class="codepen">See the Pen react.manifest.json by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
可見 manifest.json
文件清楚地描述了與其對應的 dll.js
文件中包含了哪些模塊,以及每一個模塊的路徑和 ID。
main.js
文件是編譯出來的執行入口文件,當遇到其依賴的模塊在 dll.js
文件中時,會直接經過 dll.js
文件暴露出的全局變量去獲取打包在 dll.js
文件的模塊。 因此在 index.html
文件中須要把依賴的兩個 dll.js
文件給加載進去,index.html
內容以下:
<html> <head> <meta charset="UTF-8"> </head> <body> <div id="app"></div> <!--導入依賴的動態連接庫文件--> <script src="./dist/polyfill.dll.js"></script> <script src="./dist/react.dll.js"></script> <!--導入執行入口文件--> <script src="./dist/main.js"></script> </body> </html>
以上就是全部接入 DllPlugin 後最終編譯出來的代碼,接下來教你如何實現。
構建輸出的如下這四個文件:
├── polyfill.dll.js ├── polyfill.manifest.json ├── react.dll.js └── react.manifest.json
和如下這一個文件:
├── main.js
是由兩份不一樣的構建分別輸出的。
動態連接庫文件相關的文件須要由一份獨立的構建輸出,用於給主構建使用。新建一個 Webpack 配置文件 webpack_dll.config.js
專門用於構建它們,文件內容以下:
<p data-height="665" data-theme-id="0" data-slug-hash="MGNvrB" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="webpack_dll.config.js" class="codepen">See the Pen webpack_dll.config.js by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
構建出的動態連接庫文件用於給其它地方使用,在這裏也就是給執行入口使用。
用於輸出 main.js
的主 Webpack 配置文件內容以下:
<p data-height="720" data-theme-id="0" data-slug-hash="GdVvxj" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="main.js" class="codepen">See the Pen main.js by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
注意:在webpack_dll.config.js
文件中,DllPlugin 中的name
參數必須和output.library
中保持一致。 緣由在於 DllPlugin 中的name
參數會影響輸出的manifest.json
文件中name
字段的值, 而在webpack.config.js
文件中 DllReferencePlugin 會去manifest.json
文件讀取name
字段的值, 把值的內容做爲在從全局變量中獲取動態連接庫中內容時的全局變量名。
在修改好以上兩個 Webpack 配置文件後,須要從新執行構建。 從新執行構建時要注意的是須要先把動態連接庫相關的文件編譯出來,由於主 Webpack 配置文件中定義的 DllReferencePlugin 依賴這些文件。
執行構建時流程以下:
webpack --config webpack_dll.config.js
命令。因爲有大量文件須要解析和處理,構建是文件讀寫和計算密集型的操做,特別是當文件數量變多後,Webpack 構建慢的問題會顯得嚴重。 運行在 Node.js 之上的 Webpack 是單線程模型的,也就是說 Webpack 須要處理的任務須要一件件挨着作,不能多個事情一塊兒作。
文件讀寫和計算操做是沒法避免的,那能不能讓 Webpack 同一時刻處理多個任務,發揮多核 CPU 電腦的威力,以提高構建速度呢?
HappyPack 就能讓 Webpack 作到這點,它把任務分解給多個子進程去併發的執行,子進程處理完後再把結果發送給主進程。
因爲 JavaScript 是單線程模型,要想發揮多核 CPU 的能力,只能經過多進程去實現,而沒法經過多線程實現。
分解任務和管理線程的事情 HappyPack 都會幫你作好,你所須要作的只是接入 HappyPack。 接入 HappyPack 的相關代碼以下:
<p data-height="665" data-theme-id="0" data-slug-hash="RyXLEy" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="HappyPack " class="codepen">See the Pen HappyPack by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
以上代碼有兩點重要的修改:
happypack/loader
去處理,使用緊跟其後的 querystring ?id=babel
去告訴 happypack/loader
去選擇哪一個 HappyPack 實例去處理文件。happypack/loader
去如何處理 .js
和 .css
文件。選項中的 id
屬性的值和上面 querystring
中的 ?id=babel
相對應,選項中的 loaders
屬性和 Loader 配置中同樣。在實例化 HappyPack 插件的時候,除了能夠傳入 id
和 loaders
兩個參數外,HappyPack 還支持以下參數:
threads
表明開啓幾個子進程去處理這一類型的文件,默認是3
個,類型必須是整數。verbose
是否容許 HappyPack 輸出日誌,默認是 true
。threadPool
表明共享進程池,即多個 HappyPack 實例都使用同一個共享進程池中的子進程去處理任務,以防止資源佔用過多,相關代碼以下:<p data-height="465" data-theme-id="0" data-slug-hash="MGNERw" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="threadPool " class="codepen">See the Pen threadPool by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
接入 HappyPack 後,你須要給項目安裝新的依賴:
npm i -D happypack
在整個 Webpack 構建流程中,最耗時的流程可能就是 Loader 對文件的轉換操做了,由於要轉換的文件數據巨多,並且這些轉換操做都只能一個個挨着處理。 HappyPack 的核心原理就是把這部分任務分解到多個進程去並行處理,從而減小了總的構建時間。
從前面的使用中能夠看出全部須要經過 Loader 處理的文件都先交給了 happypack/loader
去處理,收集到了這些文件的處理權後 HappyPack 就好統一分配了。
每經過 new HappyPack()
實例化一個 HappyPack 其實就是告訴 HappyPack 核心調度器如何經過一系列 Loader 去轉換一類文件,而且能夠指定如何給這類轉換操做分配子進程。
核心調度器的邏輯代碼在主進程中,也就是運行着 Webpack 的進程中,核心調度器會把一個個任務分配給當前空閒的子進程,子進程處理完畢後把結果發送給核心調度器,它們之間的數據交換是經過進程間通訊 API 實現的。
核心調度器收到來自子進程處理完畢的結果後會通知 Webpack 該文件處理完畢。
在使用 Webpack 構建出用於發佈到線上的代碼時,都會有壓縮代碼這一流程。 最多見的 JavaScript 代碼壓縮工具是 UglifyJS,而且 Webpack 也內置了它。
用過 UglifyJS 的你必定會發如今構建用於開發環境的代碼時很快就能完成,但在構建用於線上的代碼時構建一直卡在一個時間點遲遲沒有反應,其實卡住的這個時候就是在進行代碼壓縮。
因爲壓縮 JavaScript 代碼須要先把代碼解析成用 Object 抽象表示的 AST 語法樹,再去應用各類規則分析和處理 AST,致使這個過程計算量巨大,耗時很是多。
爲何不把在使用 HappyPack中介紹過的多進程並行處理的思想也引入到代碼壓縮中呢?
ParallelUglifyPlugin 就作了這個事情。 當 Webpack 有多個 JavaScript 文件須要輸出和壓縮時,本來會使用 UglifyJS 去一個個挨着壓縮再輸出, 可是 ParallelUglifyPlugin 則會開啓多個子進程,把對多個文件的壓縮工做分配給多個子進程去完成,每一個子進程其實仍是經過 UglifyJS 去壓縮代碼,可是變成了並行執行。 因此 ParallelUglifyPlugin 能更快的完成對多個文件的壓縮工做。
使用 ParallelUglifyPlugin 也很是簡單,把原來 Webpack 配置文件中內置的 UglifyJsPlugin 去掉後,再替換成 ParallelUglifyPlugin,相關代碼以下:
<p data-height="585" data-theme-id="0" data-slug-hash="BxXwgM" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="ParallelUglifyPlugin" class="codepen">See the Pen ParallelUglifyPlugin by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
在經過 new ParallelUglifyPlugin() 實例化時,支持如下參數:
test
:使用正則去匹配哪些文件須要被 ParallelUglifyPlugin 壓縮,默認是 /.js$/
,也就是默認壓縮全部的 .js
文件。include
:使用正則去命中須要被 ParallelUglifyPlugin 壓縮的文件。默認爲 []
。exclude
:使用正則去命中不須要被 ParallelUglifyPlugin 壓縮的文件。默認爲 []
。cacheDir
:緩存壓縮後的結果,下次遇到同樣的輸入時直接從緩存中獲取壓縮後的結果並返回。cacheDir 用於配置緩存存放的目錄路徑。默認不會緩存,想開啓緩存請設置一個目錄路徑。workerCount
:開啓幾個子進程去併發的執行壓縮。默認是當前運行電腦的 CPU 核數減去1。sourceMap
:是否輸出 Source Map,這會致使壓縮過程變慢。uglifyJS
:用於壓縮 ES5 代碼時的配置,Object 類型,直接透傳給 UglifyJS 的參數。uglifyES
:用於壓縮 ES6 代碼時的配置,Object 類型,直接透傳給 UglifyES 的參數。其中的 test
、include
、exclude
與配置 Loader 時的思想和用法同樣。
UglifyES 是 UglifyJS 的變種,專門用於壓縮 ES6 代碼,它們兩都出自於同一個項目,而且它們兩不能同時使用。UglifyES 通常用於給比較新的 JavaScript 運行環境壓縮代碼,例如用於 ReactNative 的代碼運行在兼容性較好的 JavaScriptCore 引擎中,爲了獲得更好的性能和尺寸,採用 UglifyES 壓縮效果會更好。
ParallelUglifyPlugin 同時內置了 UglifyJS 和 UglifyES,也就是說 ParallelUglifyPlugin 支持並行壓縮 ES6 代碼。
接入 ParallelUglifyPlugin 後,項目須要安裝新的依賴:
npm i -D webpack-parallel-uglify-plugin
安裝成功後,從新執行構建你會發現速度變快了許多。若是設置 cacheDir 開啓了緩存,在以後的構建中會變的更快。
在開發階段,修改源碼是不可避免的操做。 對於開發網頁來講,要想看到修改後的效果,須要刷新瀏覽器讓其從新運行最新的代碼才行。 雖然這相比於開發原生 iOS 和 Android 應用來講要方便不少,由於那須要從新編譯這個項目再運行,但咱們能夠把這個體驗優化的更好。 藉助自動化的手段,能夠把這些重複的操做交給代碼去幫咱們完成,在監聽到本地源碼文件發生變化時,自動從新構建出可運行的代碼後再控制瀏覽器刷新。
Webpack 把這些功能都內置了,而且還提供多種方案可選。
文件監聽是在發現源碼文件發生變化時,自動從新構建出新的輸出文件。
Webpack 官方提供了兩大模塊,一個是核心的 webpack,一個是在使用 DevServer 中提到的 webpack-dev-server
擴展模塊。 而文件監聽功能是 webpack 模塊提供的。
在其它配置項中曾介紹過 Webpack 支持文件監聽相關的配置項以下:
module.export = { // 只有在開啓監聽模式時,watchOptions 纔有意義 // 默認爲 false,也就是不開啓 watch: true, // 監聽模式運行時的參數 // 在開啓監聽模式時,纔有意義 watchOptions: { // 不監聽的文件或文件夾,支持正則匹配 // 默認爲空 ignored: /node_modules/, // 監聽到變化發生後會等300ms再去執行動做,防止文件更新太快致使從新編譯頻率過高 // 默認爲 300ms aggregateTimeout: 300, // 判斷文件是否發生變化是經過不停的去詢問系統指定文件有沒有變化實現的 // 默認每秒問 1000 次 poll: 1000 } }
要讓 Webpack 開啓監聽模式,有兩種方式:
webpack.config.js
中設置 watch: true
。--watch
參數,完整命令是 webpack --watch
。在 Webpack 中監聽一個文件發生變化的原理是定時的去獲取這個文件的最後編輯時間,每次都存下最新的最後編輯時間,若是發現當前獲取的和最後一次保存的最後編輯時間不一致,就認爲該文件發生了變化。 配置項中的 watchOptions.poll
就是用於控制定時檢查的週期,具體含義是每秒檢查多少次。
當發現某個文件發生了變化時,並不會馬上告訴監聽者,而是先緩存起來,收集一段時間的變化後,再一次性告訴監聽者。 配置項中的 watchOptions.aggregateTimeout
就是用於配置這個等待時間。 這樣作的目的是由於咱們在編輯代碼的過程當中可能會高頻的輸入文字致使文件變化的事件高頻的發生,若是每次都從新執行構建就會讓構建卡死。
對於多個文件來講,原理類似,只不過會對列表中的每個文件都定時的執行檢查。 可是這個須要監聽的文件列表是怎麼肯定的呢? 默認狀況下 Webpack 會從配置的 Entry 文件出發,遞歸解析出 Entry 文件所依賴的文件,把這些依賴的文件都加入到監聽列表中去。 可見 Webpack 這一點仍是作的很智能的,不是粗暴的直接監聽項目目錄下的全部文件。
因爲保存文件的路徑和最後編輯時間須要佔用內存,定時檢查週期檢查須要佔用 CPU 以及文件 I/O,因此最好減小須要監聽的文件數量和下降檢查頻率。
在明白文件監聽工做原理後,就好分析如何優化文件監聽性能了。
開啓監聽模式時,默認狀況下會監聽配置的 Entry 文件和全部其遞歸依賴的文件。 在這些文件中會有不少存在於 node_modules
下,由於現在的 Web 項目會依賴大量的第三方模塊。 在大多數狀況下咱們都不可能去編輯 node_modules
下的文件,而是編輯本身創建的源碼文件。 因此一個很大的優化點就是忽略掉 node_modules
下的文件,不監聽它們。相關配置以下:
module.export = { watchOptions: { // 不監聽的 node_modules 目錄下的文件 ignored: /node_modules/, } }
採用這種方法優化後,你的 Webpack 消耗的內存和 CPU 將會大大下降。
有時你可能會以爲node_modules
目錄下的第三方模塊有bug
,想修改第三方模塊的文件,而後在本身的項目中試試。 在這種狀況下若是使用了以上優化方法,咱們須要重啓構建以看到最新效果。 但這種狀況畢竟是很是少見的。
除了忽略掉部分文件的優化外,還有以下兩種方法:
watchOptions.aggregateTimeout
值越大性能越好,由於這能下降從新構建的頻率。watchOptions.poll
值越小越好,由於這能下降檢查的頻率。但兩種優化方法的後果是會讓你感受到監聽模式的反應和靈敏度下降了。
監聽到文件更新後的下一步是去刷新瀏覽器,webpack
模塊負責監聽文件,webpack-dev-server
模塊則負責刷新瀏覽器。 在使用 webpack-dev-server
模塊去啓動 webpack
模塊時,webpack
模塊的監聽模式默認會被開啓。 webpack
模塊會在文件發生變化時告訴 webpack-dev-server
模塊。
控制瀏覽器刷新有三種方法:
iframe
中,經過刷新 iframe
去看到最新效果。DevServer 支持第二、3種方法,第2種是 DevServer 默認採用的刷新方法。
在DevServer中曾介紹過 devServer.inline
配置項,它就是用來控制是否往 Chunk 中注入代理客戶端的,默認會注入。 事實上,在開啓 inline
時,DevServer 會爲每一個輸出的 Chunk 中注入代理客戶端的代碼,當你的項目須要輸出的 Chunk 有不少個時,這會致使你的構建緩慢。 其實要完成自動刷新,一個頁面只須要一個代理客戶端就好了,DevServer 之因此粗暴的爲每一個 Chunk 都注入,是由於它不知道某個網頁依賴哪幾個 Chunk,索性就所有都注入一個代理客戶端。 網頁只要依賴了其中任何一個 Chunk,代理客戶端就被注入到網頁中去。
這裏優化的思路是關閉還不夠優雅的 inline
模式,只注入一個代理客戶端。 爲了關閉 inline
模式,在啓動 DevServer 時,可經過執行命令 webpack-dev-server --inline false
(也能夠在配置文件中設置)。
要開發的網頁被放進了一個 iframe
中,編輯源碼後,iframe
會被自動刷新。 同時你會發現構建時間從 1566ms
減小到了 1130ms
,說明優化生效了。構建性能提高的效果在要輸出的 Chunk 數量越多時會顯得越突出。
在你關閉了inline
後,DevServer 會自動地提示你經過新網址http://localhost:8080/webpack-dev-server/
去訪問,這點是作的很人心化的。
若是你不想經過 iframe
的方式去訪問,但同時又想讓網頁保持自動刷新功能,你須要手動往網頁中注入代理客戶端腳本,往 index.html
中插入如下標籤:
<!--注入 DevServer 提供的代理客戶端腳本,這個服務是 DevServer 內置的--> <script src="http://localhost:8080/webpack-dev-server.js"></script>
給網頁注入以上腳本後,獨立打開的網頁就能自動刷新了。可是要注意在發佈到線上時記得刪除掉這段用於開發環境的代碼。
要作到實時預覽,除了在使用自動刷新中介紹的刷新整個網頁外,DevServer 還支持一種叫作模塊熱替換(Hot Module Replacement)的技術可在不刷新整個網頁的狀況下作到超靈敏的實時預覽。 原理是當一個源碼發生變化時,只從新編譯發生變化的模塊,再用新輸出的模塊替換掉瀏覽器中對應的老模塊。
模塊熱替換技術的優點有:
總的來講模塊熱替換技術很大程度上的提升了開發效率和體驗。
模塊熱替換的原理和自動刷新原理相似,都須要往要開發的網頁中注入一個代理客戶端用於鏈接 DevServer 和網頁, 不一樣在於模塊熱替換獨特的模塊替換機制。
DevServer 默認不會開啓模塊熱替換模式,要開啓該模式,只需在啓動時帶上參數 --hot
,完整命令是 webpack-dev-server --hot
。
除了經過在啓動時帶上 --hot
參數,還能夠經過接入 Plugin 實現,相關代碼以下:
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin'); module.exports = { entry:{ // 爲每一個入口都注入代理客戶端 main:['webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/dev-server','./src/main.js'], }, plugins: [ // 該插件的做用就是實現模塊熱替換,實際上當啓動時帶上 `--hot` 參數,會注入該插件,生成 .hot-update.json 文件。 new HotModuleReplacementPlugin(), ], devServer:{ // 告訴 DevServer 要開啓模塊熱替換模式 hot: true, } };
在啓動 Webpack 時帶上參數 --hot
其實就是自動爲你完成以上配置。
相比於自動刷新的代理客戶端,多出了後三個用於模塊熱替換的文件,也就是說代理客戶端更大了。
可見補丁中包含了 main.css
文件新編譯出來 CSS 代碼,網頁中的樣式也馬上變成了源碼中描述的那樣。
但當你修改 main.js
文件時,會發現模塊熱替換沒有生效,而是整個頁面被刷新了,爲何修改 main.js 文件時會這樣呢?
Webpack 爲了讓使用者在使用了模塊熱替換功能時能靈活地控制老模塊被替換時的邏輯,能夠在源碼中定義一些代碼去作相應的處理。
把的 main.js
文件改成以下:
<p data-height="365" data-theme-id="0" data-slug-hash="QreOEw" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="main.js" class="codepen">See the Pen main.js by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
其中的 module.hot
是當開啓模塊熱替換後注入到全局的 API,用於控制模塊熱替換的邏輯。
如今修改 AppComponent.js
文件,把 Hello,Webpack
改爲 Hello,World
,你會發現模塊熱替換生效了。 可是當你編輯 main.js
時,你會發現整個網頁被刷新了。爲何修改這兩個文件會有不同的表現呢?
當子模塊發生更新時,更新事件會一層層往上傳遞,也就是從 AppComponent.js
文件傳遞到 main.js
文件, 直到有某層的文件接受了當前變化的模塊,也就是 main.js
文件中定義的 module.hot.accept(['./AppComponent'], callback)
, 這時就會調用 callback
函數去執行自定義邏輯。若是事件一直往上拋到最外層都沒有文件接受它,就會直接刷新網頁。
那爲何沒有地方接受過 .css
文件,可是修改全部的 .css
文件都會觸發模塊熱替換呢? 緣由在於 style-loader
會注入用於接受 CSS 的代碼。
請不要把模塊熱替換技術用於線上環境,它是專門爲提高開發效率生的。
其中的 Updated modules: 68 是指 ID 爲68的模塊被替換了,這對開發者來講很不友好,由於開發者不知道 ID 和模塊之間的對應關係,最好是把替換了的模塊的名稱輸出出來。 Webpack 內置的 NamedModulesPlugin 插件能夠解決該問題,修改 Webpack 配置文件接入該插件:
const NamedModulesPlugin = require('webpack/lib/NamedModulesPlugin'); module.exports = { plugins: [ // 顯示出被替換模塊的名稱 new NamedModulesPlugin(), ], };
除此以外,模塊熱替換還面臨着和自動刷新同樣的性能問題,由於它們都須要監聽文件變化和注入客戶端。 要優化模塊熱替換的構建性能,思路和在使用自動刷新中提到的很相似:監聽更少的文件,忽略掉 node_modules
目錄下的文件。 可是其中提到的關閉默認的 inline
模式手動注入代理客戶端的優化方法不能用於在使用模塊熱替換的狀況下, 緣由在於模塊熱替換的運行依賴在每一個 Chunk 中都包含代理客戶端的代碼。
在開發網頁的時候,通常都會有多套運行環境,例如:
這兩套不一樣的環境雖然都是由同一套源代碼編譯而來,可是代碼內容卻不同,差別包括:
爲了儘量的複用代碼,在構建的過程當中須要根據目標代碼要運行的環境而輸出不一樣的代碼,咱們須要一套機制在源碼中去區分環境。 幸運的是 Webpack 已經爲咱們實現了這點。
具體區分方法很簡單,在源碼中經過以下方式:
if (process.env.NODE_ENV === 'production') { console.log('你正在線上環境'); } else { console.log('你正在使用開發環境'); }
其大概原理是藉助於環境變量的值去判斷執行哪一個分支。
當你的代碼中出現了使用 process
模塊的語句時,Webpack 就自動打包進 process
模塊的代碼以支持非 Node.js 的運行環境。 當你的代碼中沒有使用 process
時就不會打包進 process
模塊的代碼。這個注入的 process 模塊做用是爲了模擬 Node.js 中的 process
,以支持上面使用的 process.env.NODE_ENV === 'production'
語句。
在構建線上環境代碼時,須要給當前運行環境設置環境變量 NODE_ENV = 'production',Webpack
相關配置以下:
const DefinePlugin = require('webpack/lib/DefinePlugin'); module.exports = { plugins: [ new DefinePlugin({ // 定義 NODE_ENV 環境變量爲 production 'process.env': { NODE_ENV: JSON.stringify('production') } }), ], };
注意在定義環境變量的值時用JSON.stringify
包裹字符串的緣由是環境變量的值須要是一個由雙引號包裹的字符串,而JSON.stringify('production')
的值正好等於'"production"'
。
執行構建後,你會在輸出的文件中發現以下代碼:
if (true) { console.log('你正在使用線上環境'); } else { console.log('你正在使用開發環境'); }
定義的環境變量的值被代入到了源碼中,process.env.NODE_ENV === 'production'
被直接替換成了 true
。 而且因爲此時訪問 process
的語句被替換了而沒有了,Webpack 也不會打包進 process
模塊了。
DefinePlugin 定義的環境變量只對 Webpack
須要處理的代碼有效,而不會影響 Node.js 運行時的環境變量的值。
經過 Shell 腳本的方式去定義的環境變量,例如 NODE_ENV=production webpack,Webpack
是不認識的,對 Webpack 須要處理的代碼中的環境區分語句是沒有做用的。
也就是說只須要經過 DefinePlugin 定義環境變量就能使上面介紹的環境區分語句正常工做,不必又經過 Shell 腳本的方式去定義一遍。
若是你想讓 Webpack 使用經過 Shell 腳本的方式去定義的環境變量,你可使用 EnvironmentPlugin
,代碼以下:
new webpack.EnvironmentPlugin(['NODE_ENV'])
以上這句代碼實際上等價於:
new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), })
其實以上輸出的代碼還能夠進一步優化,由於 if(true)
語句永遠只會執行前一個分支中的代碼,也就是說最佳的輸出其實應該直接是:
console.log('你正在線上環境');
Webpack 沒有實現去除死代碼功能,可是 UglifyJS 能夠作這個事情,如何使用請閱讀 壓縮代碼 中的壓縮 JavaScript。
除了在本身寫的源碼中能夠有環境區分的代碼外,不少第三方庫也作了環境區分的優化。 以 React 爲例,它作了兩套環境區分,分別是:
例如 React 源碼中有大量相似下面這樣的代碼:
if (process.env.NODE_ENV !== 'production') { warning(false, '%s(...): Can only update a mounted or mounting component.... ') }
若是你不定義 NODE_ENV=production
那麼這些警告日誌就會被包含到輸出的代碼中,輸出的文件將會很是大。
process.env.NODE_ENV !== 'production'
中的 NODE_ENV
和 'production'
兩個值是社區的約定,一般使用這條判斷語句在區分開發環境和線上環境。
瀏覽器從服務器訪問網頁時獲取的 JavaScript、CSS 資源都是文本形式的,文件越大網頁加載時間越長。 爲了提高網頁加速速度和減小網絡傳輸流量,能夠對這些資源進行壓縮。 壓縮的方法除了能夠經過 GZIP
算法對文件壓縮外,還能夠對文本自己進行壓縮。
對文本自己進行壓縮的做用除了有提高網頁加載速度的優點外,還具備混淆源碼的做用。 因爲壓縮後的代碼可讀性很是差,就算別人下載到了網頁的代碼,也大大增長了代碼分析和改造的難度。
下面來一一介紹如何在 Webpack 中壓縮代碼。
目前最成熟的 JavaScript 代碼壓縮工具是 UglifyJS , 它會分析 JavaScript 代碼語法樹,理解代碼含義,從而能作到諸如去掉無效代碼、去掉日誌輸出代碼、縮短變量名等優化。
要在 Webpack 中接入 UglifyJS 須要經過插件的形式,目前有兩個成熟的插件,分別是:
UglifyJsPlugin
:經過封裝 UglifyJS 實現壓縮。ParallelUglifyPlugin
:多進程並行處理壓縮,使用 ParallelUglifyPlugin 中有詳細介紹。因爲 ParallelUglifyPlugin 在 4-4使用ParallelUglifyPlugin 中介紹過就再也不復述, 這裏重點介紹如何配置 UglifyJS 以達到最優的壓縮效果。
UglifyJS 提供了很是多的選擇用於配置在壓縮過程當中採用哪些規則,全部的選項說明能夠在 其官方文檔 上看到。 因爲選項很是多,就挑出一些經常使用的拿出來詳細講解其應用方式:
sourceMap
:是否爲壓縮後的代碼生成對應的 Source Map,默認爲不生成,開啓後耗時會大大增長。通常不會把壓縮後的代碼的 Source Map 發送給網站用戶的瀏覽器,而是用於內部開發人員調試線上代碼時使用。beautify
: 是否輸出可讀性較強的代碼,即會保留空格和製表符,默認爲是,爲了達到更好的壓縮效果,能夠設置爲 false。comments
:是否保留代碼中的註釋,默認爲保留,爲了達到更好的壓縮效果,能夠設置爲 false
。compress.warnings
:是否在 UglifyJs 刪除沒有用到的代碼時輸出警告信息,默認爲輸出,能夠設置爲 false
以關閉這些做用不大的警告。drop_console
:是否剔除代碼中全部的 console
語句,默認爲不剔除。開啓後不只能夠提高代碼壓縮效果,也能夠兼容不支持 console
語句 IE 瀏覽器。collapse_vars
:是否內嵌定義了可是隻用到一次的變量,例如把 var x = 5; y = x
轉換成 y = 5
,默認爲不轉換。爲了達到更好的壓縮效果,能夠設置爲 false
。reduce_vars
: 是否提取出出現屢次可是沒有定義成變量去引用的靜態值,例如把 x = 'Hello'; y = 'Hello'
轉換成 var a = 'Hello'; x = a; y = b
,默認爲不轉換。爲了達到更好的壓縮效果,能夠設置爲 false
。也就是說,在不影響代碼正確執行的前提下,最優化的代碼壓縮配置爲以下:
const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); module.exports = { plugins: [ // 壓縮輸出的 JS 代碼 new UglifyJSPlugin({ compress: { // 在UglifyJs刪除沒有用到的代碼時不輸出警告 warnings: false, // 刪除全部的 `console` 語句,能夠兼容ie瀏覽器 drop_console: true, // 內嵌定義了可是隻用到一次的變量 collapse_vars: true, // 提取出出現屢次可是沒有定義成變量去引用的靜態值 reduce_vars: true, }, output: { // 最緊湊的輸出 beautify: false, // 刪除全部的註釋 comments: false, } }), ], };
從以上配置中能夠看出 Webpack 內置了 UglifyJsPlugin,須要指出的是 UglifyJsPlugin 當前採用的是 UglifyJS2 而不是老的 UglifyJS1, 這兩個版本的 UglifyJS 在配置上有所區別,看文檔時注意版本。
除此以外 Webpack 還提供了一個更簡便的方法來接入 UglifyJSPlugin,直接在啓動 Webpack 時帶上 --optimize-minimize
參數,即 webpack --optimize-minimize
, 這樣 Webpack 會自動爲你注入一個帶有默認配置的 UglifyJSPlugin。
雖然當前大多數 JavaScript 引擎還不徹底支持 ES6 中的新特性,但在一些特定的運行環境下已經能夠直接執行 ES6 代碼了,例如最新版的 Chrome、ReactNative 的引擎 JavaScriptCore。
運行 ES6 的代碼相比於轉換後的 ES5 代碼有以下優勢:
const
申明的變量有更快的讀取速度。因此在運行環境容許的狀況下,咱們要儘量的使用原生的 ES6 代碼去運行,而不是轉換後的 ES5 代碼。
在你用上面所講的壓縮方法去壓縮 ES6 代碼時,你會發現 UglifyJS 會報錯退出,緣由是 UglifyJS 只認識 ES5 語法的代碼。 爲了壓縮 ES6 代碼,須要使用專門針對 ES6 代碼的 UglifyES。
UglifyES 和 UglifyJS 來自同一個項目的不一樣分支,它們的配置項基本相同,只是接入 Webpack 時有所區別。 在給 Webpack 接入 UglifyES 時,不能使用內置的 UglifyJsPlugin,而是須要單獨安裝和使用最新版本的 uglifyjs-webpack-plugin
。 安裝方法以下:
npm i -D uglifyjs-webpack-plugin@beta
Webpack 相關配置代碼以下:
<p data-height="465" data-theme-id="0" data-slug-hash="ELqbWw" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="Webpack" class="codepen">See the Pen Webpack by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
同時,爲了避免讓 babel-loader
輸出 ES5 語法的代碼,須要去掉 .babelrc
配置文件中的 babel-preset-env
,可是其它的 Babel 插件,好比 babel-preset-react
仍是要保留, 由於正是 babel-preset-env
負責把 ES6 代碼轉換爲 ES5 代碼。
CSS 代碼也能夠像 JavaScript 那樣被壓縮,以達到提高加載速度和代碼混淆的做用。 目前比較成熟可靠的 CSS 壓縮工具是 cssnano,基於 PostCSS。
cssnano
能理解 CSS 代碼的含義,而不只僅是刪掉空格,例如:
margin: 10px 20px 10px 20px
被壓縮成 margin: 10px 20px
color: #ff0000
被壓縮成 color:red
還有不少壓縮規則能夠去其官網查看,一般壓縮率能達到 60%。
把 cssnano
接入到 Webpack 中也很是簡單,由於 css-loader
已經將其內置了,要開啓 cssnano
去壓縮代碼只須要開啓 css-loader
的 minimize
選項。 相關 Webpack 配置以下:
<p data-height="565" data-theme-id="0" data-slug-hash="rvXYwm" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="cssnano" class="codepen">See the Pen cssnano by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
雖然前面經過了壓縮代碼的手段來減少網絡傳輸大小,但實際上最影響用戶體驗的仍是網頁首次打開時的加載等待。 致使這個問題的根本是網絡傳輸過程耗時大,CDN 的做用就是加速網絡傳輸。
CDN 又叫內容分發網絡
,經過把資源部署到世界各地,用戶在訪問時按照就近原則從離用戶最近的服務器獲取資源,從而加速資源的獲取速度。 CDN 實際上是經過優化物理鏈路層傳輸過程當中的光速有限、丟包等問題來提高網速的,其大體原理能夠以下:
在本節中你沒必要理解 CDN 的具體運行流程和實現原理,你能夠簡單的把 CDN 服務看做成速度更快的 HTTP 服務。 而且目前不少大公司都會創建本身的 CDN 服務,就算你本身沒有資源去搭建一套 CDN 服務,各大雲服務提供商都提供了按量收費的 CDN 服務。
要給網站接入 CDN,須要把網頁的靜態資源上傳到 CDN 服務上去,在服務這些靜態資源的時候須要經過 CDN 服務提供的 URL 地址去訪問。
舉個詳細的例子,有一個單頁應用,構建出的代碼結構以下:
dist |-- app_9d89c964.js |-- app_a6976b6d.css |-- arch_ae805d49.png `-- index.html
其中 index.html
內容以下:
<html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="app_a6976b6d.css"> </head> <body> <div id="app"></div> <script src="app_9d89c964.js"></script> </body> </html>
app_a6976b6d.css
內容以下:
body{background:url(arch_ae805d49.png) repeat}h1{color:red}
能夠看出到導入資源時都是經過相對路徑去訪問的,當把這些資源都放到同一個 CDN 服務上去時,網頁是能正常使用的。 但須要注意的是因爲 CDN 服務通常都會給資源開啓很長時間的緩存,例如用戶從 CDN 上獲取到了 index.html
這個文件後, 即便以後的發佈操做把 index.html
文件給從新覆蓋了,可是用戶在很長一段時間內仍是運行的以前的版本,這會新的致使發佈不能當即生效。
要避免以上問題,業界比較成熟的作法是這樣的:
app_a6976b6d.css
文件。 帶上 Hash 值的緣由是文件名會隨着文件內容而變化,只要文件發生變化其對應的 URL 就會變化,它就會被從新下載,不管緩存時間有多長。採用以上方案後,在 HTML 文件中的資源引入地址也須要換成 CDN 服務提供的地址,例如以上的 index.html
變爲以下:
<html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="//cdn.com/id/app_a6976b6d.css"> </head> <body> <div id="app"></div> <script src="//cdn.com/id/app_9d89c964.js"></script> </body> </html>
而且 app_a6976b6d.css
的內容也應該變爲以下:
也就是說,以前的相對路徑,都變成了絕對的指向 CDN 服務的 URL 地址。
若是你對形如//cdn.com/id/app_a6976b6d.css
這樣的 URL 感到陌生,你須要知道這種 URL 省掉了前面的http:
或者https:
前綴, 這樣作的好處時在訪問這些資源的時候會自動的根據當前 HTML 的 URL 是採用什麼模式去決定是採用 HTTP 仍是 HTTPS 模式。
除此以外,若是你還知道瀏覽器有一個規則是同一時刻針對同一個域名的資源並行請求是有限制的話(具體數字大概4個左右,不一樣瀏覽器可能不一樣), 你會發現上面的作法有個很大的問題。因爲全部靜態資源都放到了同一個 CDN 服務的域名下,也就是上面的 cdn.com
。 若是網頁的資源不少,例若有不少圖片,就會致使資源的加載被阻塞,由於同時只能加載幾個,必須等其它資源加載完才能繼續加載。 要解決這個問題,能夠把這些靜態資源分散到不一樣的 CDN 服務上去, 例如把 JavaScript 文件放到 js.cdn.com
域名下、把 CSS 文件放到 css.cdn.com
域名下、圖片文件放到 img.cdn.com
域名下, 這樣作以後 index.html
須要變成這樣:
<html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="//css.cdn.com/id/app_a6976b6d.css"> </head> <body> <div id="app"></div> <script src="//js.cdn.com/id/app_9d89c964.js"></script> </body> </html>
使用了多個域名後又會帶來一個新問題:增長域名解析時間。是否採用多域名分散資源須要根據本身的需求去衡量得失。 固然你能夠經過在 HTML HEAD 標籤中 加入 <link rel="dns-prefetch" href="//js.cdn.com"> 去預解析域名,以下降域名解析帶來的延遲。
總結上面所說的,構建須要實現如下幾點:
先來看下要實現以上要求的最終 Webpack 配置:
<p data-height="565" data-theme-id="0" data-slug-hash="ELqbwb" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="CDN 的接入" class="codepen">See the Pen CDN 的接入 by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
以上代碼中最核心的部分是經過 publicPath
參數設置存放靜態資源的 CDN 目錄 URL, 爲了讓不一樣類型的資源輸出到不一樣的 CDN,須要分別在:
output.publicPath
中設置 JavaScript 的地址。css-loader.publicPath
中設置被 CSS 導入的資源的的地址。WebPlugin.stylePublicPath
中設置 CSS 文件的地址。設置好 publicPath
後,WebPlugin 在生成 HTML 文件和 css-loader
轉換 CSS 代碼時,會考慮到配置中的 publicPath
,用對應的線上地址替換原來的相對地址。
Tree Shaking 能夠用來剔除 JavaScript 中用不上的死代碼。它依賴靜態的 ES6 模塊化語法,例如經過 import
和 export
導入導出。 Tree Shaking 最早在 Rollup 中出現,Webpack 在 2.0 版本中將其引入。
爲了更直觀的理解它,來看一個具體的例子。假若有一個文件 util.js
裏存放了不少工具函數和常量,在 main.js
中會導入和使用 util.js
,代碼以下:
util.js
源碼:
export function funcA() { } export function funB() { }
main.js
源碼:
import {funcA} from './util.js'; funcA();
Tree Shaking 後的 util.js
:
export function funcA() { }
因爲只用到了 util.js
中的 funcA
,因此剩下的都被 Tree Shaking 看成死代碼給剔除了。
須要注意的是要讓 Tree Shaking 正常工做的前提是交給 Webpack 的 JavaScript 代碼必須是採用 ES6 模塊化語法的, 由於 ES6 模塊化語法是靜態的(導入導出語句中的路徑必須是靜態的字符串,並且不能放入其它代碼塊中),這讓 Webpack 能夠簡單的分析出哪些 export
的被 import
過了。 若是你採用 ES5 中的模塊化,例如 module.export={...}
、require(x+y)
、if(x){require('./util')}
,Webpack 沒法分析出哪些代碼能夠剔除。
上面講了 Tree Shaking 是作什麼的,接下來一步步教你如何配置 Webpack 讓 Tree Shaking 生效。
首先,爲了把採用 ES6 模塊化的代碼交給 Webpack,須要配置 Babel 讓其保留 ES6 模塊化語句,修改 .babelrc
文件爲以下:
{ "presets": [ [ "env", { "modules": false } ] ] }
其中 "modules": false
的含義是關閉 Babel 的模塊轉換功能,保留本來的 ES6 模塊化語法。
配置好 Babel 後,從新運行 Webpack,在啓動 Webpack 時帶上 --display-used-exports
參數,以方便追蹤 Tree Shaking 的工做, 這時你會發如今控制檯中輸出了以下的日誌:
> webpack --display-used-exports bundle.js 3.5 kB 0 [emitted] main [0] ./main.js 41 bytes {0} [built] [1] ./util.js 511 bytes {0} [built] [only some exports used: funcA]
其中 [only some exports used: funcA] 提示了 util.js 只導出了用到的 funcA,說明 Webpack 確實正確的分析出瞭如何剔除死代碼。
但當你打開 Webpack 輸出的 bundle.js
文件看下時,你會發現用不上的代碼還在裏面,以下:
/* harmony export (immutable) */ __webpack_exports__["a"] = funcA; /* unused harmony export funB */ function funcA() { console.log('funcA'); } function funB() { console.log('funcB'); }
Webpack 只是指出了哪些函數用上了哪些沒用上,要剔除用不上的代碼還得通過 UglifyJS 去處理一遍。 要接入 UglifyJS 也很簡單,不只能夠經過4-8壓縮代碼中介紹的加入 UglifyJSPlugin 去實現, 也能夠簡單的經過在啓動 Webpack 時帶上 --optimize-minimize
參數,爲了快速驗證 Tree Shaking 咱們採用較簡單的後者來實驗下。
經過 webpack --display-used-exports --optimize-minimize
重啓 Webpack 後,打開新輸出的 bundle.js
,內容以下:
function r() { console.log("funcA") } t.a = r
能夠看出 Tree Shaking 確實作到了,用不上的代碼都被剔除了。
當你的項目使用了大量第三方庫時,你會發現 Tree Shaking 彷佛不生效了,緣由是大部分 Npm 中的代碼都是採用的 CommonJS 語法, 這致使 Tree Shaking 沒法正常工做而降級處理。 但幸運的時有些庫考慮到了這點,這些庫在發佈到 Npm 上時會同時提供兩份代碼,一份採用 CommonJS 模塊化語法,一份採用 ES6 模塊化語法。 而且在 package.json
文件中分別指出這兩份代碼的入口。
以 redux
庫爲例,其發佈到 Npm 上的目錄結構爲:
node_modules/redux |-- es | |-- index.js # 採用 ES6 模塊化語法 |-- lib | |-- index.js # 採用 ES5 模塊化語法 |-- package.json
package.json
文件中有兩個字段:
{ "main": "lib/index.js", // 指明採用 CommonJS 模塊化的代碼入口 "jsnext:main": "es/index.js" // 指明採用 ES6 模塊化的代碼入口 }
mainFields
用於配置採用哪一個字段做爲模塊的入口描述。 爲了讓 Tree Shaking 對 redux
生效,須要配置 Webpack 的文件尋找規則爲以下:
module.exports = { resolve: { // 針對 Npm 中的第三方模塊優先採用 jsnext:main 中指向的 ES6 模塊化語法的文件 mainFields: ['jsnext:main', 'browser', 'main'] }, };
以上配置的含義是優先使用 jsnext:main
做爲入口,若是不存在 jsnext:main
就採用 browser
或者 main
做爲入口。 雖然並非每一個 Npm 中的第三方模塊都會提供 ES6 模塊化語法的代碼,但對於提供了的不能放過,能優化的就優化。
目前愈來愈多的 Npm 中的第三方模塊考慮到了 Tree Shaking,並對其提供了支持。 採用 jsnext:main
做爲 ES6 模塊化代碼的入口是社區的一個約定,假如未來你要發佈一個庫到 Npm 時,但願你能支持 Tree Shaking, 以讓 Tree Shaking 發揮更大的優化效果,讓更多的人爲此受益。
大型網站一般會由多個頁面組成,每一個頁面都是一個獨立的單頁應用。 但因爲全部頁面都採用一樣的技術棧,以及使用同一套樣式代碼,這致使這些頁面之間有不少相同的代碼。
若是每一個頁面的代碼都把這些公共的部分包含進去,會形成如下問題:
若是把多個頁面公共的代碼抽離成單獨的文件,就能優化以上問題。 緣由是假如用戶訪問了網站的其中一個網頁,那麼訪問這個網站下的其它網頁的機率將很是大。 在用戶第一次訪問後,這些頁面公共代碼的文件已經被瀏覽器緩存起來,在用戶切換到其它頁面時,存放公共代碼的文件就不會再從新加載,而是直接從緩存中獲取。 這樣作後有以下好處:
你已經知道了提取公共代碼會有什麼好處,可是在實戰中具體要怎麼作,以達到效果最優呢? 一般你能夠採用如下原則去爲你的網站提取公共代碼:
react
、react-dom
等庫,把它們提取到一個單獨的文件。 通常把這個文件叫作 base.js,由於它包含全部網頁的基礎運行環境;base.js
包含的部分代碼外,再找出全部頁面都依賴的公共部分的代碼提取出來放到 common.js
中去。base.js
和 common.js
中包含的部分,而只包含各個頁面單獨須要的部分代碼。文件之間的結構圖以下:
讀到這裏你能夠會有疑問:既然能找出全部頁面都依賴的公共代碼,並提取出來放到 common.js
中去,爲何還須要再把網站全部頁面都須要用到的基礎庫提取到 base.js
去呢? 緣由是爲了長期的緩存 base.js
這個文件。
發佈到線上的文件都會採用在4-9CDN加速中介紹過的方法,對靜態文件的文件名都附加根據文件內容計算出 Hash 值,也就是最終 base.js
的文件名會變成 base_3b1682ac.js
,以長期緩存文件。 網站一般會不斷的更新發布,每次發佈都會致使 common.js
和各個網頁的 JavaScript 文件都會由於文件內容發生變化而致使其 Hash 值被更新,也就是緩存被更新。
把全部頁面都須要用到的基礎庫提取到 base.js
的好處在於只要不升級基礎庫的版本,base.js
的文件內容就不會變化,Hash 值不會被更新,緩存就不會被更新。 每次發佈瀏覽器都會使用被緩存的 base.js
文件,而不用去從新下載 base.js
文件。 因爲 base.js
一般會很大,這對提高網頁加速速度能起到很大的效果。
你已經知道如何提取公共代碼,接下來教你如何用 Webpack 實現。
Webpack 內置了專門用於提取多個 Chunk 中公共部分的插件 CommonsChunkPlugin
,CommonsChunkPlugin
大體使用方法以下:
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin'); new CommonsChunkPlugin({ // 從哪些 Chunk 中提取 chunks: ['a', 'b'], // 提取出的公共部分造成一個新的 Chunk,這個新 Chunk 的名稱 name: 'common' })
以上配置就能從網頁 A 和網頁 B 中抽離出公共部分,放到 common
中。
每一個 CommonsChunkPlugin 實例都會生成一個新的 Chunk,這個新 Chunk 中包含了被提取出的代碼,在使用過程當中必須指定 name
屬性,以告訴插件新生成的 Chunk 的名稱。 其中 chunks
屬性指明從哪些已有的 Chunk 中提取,若是不填該屬性,則默認會從全部已知的 Chunk 中提取。
Chunk 是一系列文件的集合,一個 Chunk 中會包含這個 Chunk 的入口文件和入口文件依賴的文件。
經過以上配置輸出的 common Chunk 中會包含全部頁面都依賴的基礎運行庫 react
、react-dom
,爲了把基礎運行庫從 common
中抽離到 base
中去,還須要作一些處理。
首先須要先配置一個 Chunk,這個 Chunk 中只依賴全部頁面都依賴的基礎庫以及全部頁面都使用的樣式,爲此須要在項目中寫一個文件 base.js
來描述 base Chunk 所依賴的模塊,文件內容以下:
// 全部頁面都依賴的基礎庫 import 'react'; import 'react-dom'; // 全部頁面都使用的樣式 import './base.css';
接着再修改 Webpack 配置,在 entry
中加入 base
,相關修改以下:
module.exports = { entry: { base: './base.js' }, };
以上就完成了對新 Chunk base 的配置。
爲了從 common 中提取出 base
也包含的部分,還須要配置一個 CommonsChunkPlugin
,相關代碼以下:
new CommonsChunkPlugin({ // 從 common 和 base 兩個現成的 Chunk 中提取公共的部分 chunks: ['common', 'base'], // 把公共的部分放到 base 中 name: 'base' })
因爲 common
和 base
公共的部分就是 base
目前已經包含的部分,因此這樣配置後 common
將會變小,而 base
將保持不變。
以上都配置好後從新執行構建,你將會獲得四個文件,它們分別是:
base.js
:全部網頁都依賴的基礎庫組成的代碼;common.js
:網頁A、B都須要的,但又不在 base.js
文件中出現過的代碼;a.js
:網頁 A 單獨須要的代碼;b.js
:網頁 B 單獨須要的代碼。
爲了讓網頁正常運行,以網頁 A
爲例,你須要在其 HTML 中按照如下順序引入如下文件才能讓網頁正常運行:
<script src="base.js"></script> <script src="common.js"></script> <script src="a.js"></script>
以上就完成了提取公共代碼須要的全部步驟。
針對 CSS 資源,以上理論和方法一樣有效,也就是說你也能夠對 CSS 文件作一樣的優化。
以上方法可能會出現 common.js
中沒有代碼的狀況,緣由是去掉基礎運行庫外很難再找到全部頁面都會用上的模塊。 在出現這種狀況時,你能夠採起如下作法之一:
minChunks
,表示文件要被提取出來時須要在指定的 Chunks 中最小出現最小次數。 假如 minChunks=二、chunks=['a','b','c','d']
,任何一個文件只要在 ['a','b','c','d']
中任意兩個以上的 Chunk 中都出現過,這個文件就會被提取出來。 你能夠根據本身的需求去調整 minChunks
的值,minChunks
越小越多的文件會被提取到 common.js
中去,但這也會致使部分頁面加載的不相關的資源越多; minChunks
越大越少的文件會被提取到 common.js
中去,但這會致使 common.js
變小、效果變弱。CommonsChunkPlugin
去提取這部分被選出的頁面的公共部分,而不是提取全部頁面的公共部分,並且這樣的操做能夠疊加屢次。 這樣作的效果會很好,但缺點是配置複雜,你須要根據頁面之間的關係去思考如何配置,該方法不通用。本實例提供 項目完整代碼
隨着互聯網的發展,一個網頁須要承載的功能愈來愈多。 對於採用單頁應用做爲前端架構的網站來講,會面臨着一個網頁須要加載的代碼量很大的問題,由於許多功能都集中的作到了一個 HTML 裏。 這會致使網頁加載緩慢、交互卡頓,用戶體驗將很是糟糕。
致使這個問題的根本緣由在於一次性的加載全部功能對應的代碼,但其實用戶每一階段只可能使用其中一部分功能。 因此解決以上問題的方法就是用戶當前須要用什麼功能就只加載這個功能對應的代碼,也就是所謂的按需加載。
在給單頁應用作按需加載優化時,通常採用如下原則:
Chart.js
去畫圖表、依賴 flv.js
去播放視頻的功能點,可再對其進行按需加載。被分割出去的代碼的加載須要必定的時機去觸發,也就是當用戶操做到了或者即將操做到對應的功能時再去加載對應的代碼。 被分割出去的代碼的加載時機須要開發者本身去根據網頁的需求去衡量和肯定。
因爲被分割出去進行按需加載的代碼在加載的過程當中也須要耗時,你能夠預言用戶接下來可能會進行的操做,並提早加載好對應的代碼,從而讓用戶感知不到網絡加載時間。
Webpack 內置了強大的分割代碼的功能去實現按需加載,實現起來很是簡單。
舉個例子,如今須要作這樣一個進行了按需加載優化的網頁:
main.js
文件,網頁會展現一個按鈕,main.js
文件中只包含監聽按鈕事件和加載按需加載的代碼。show.js
文件,加載成功後再執行 show.js
裏的函數。其中 main.js
文件內容以下:
window.document.getElementById('btn').addEventListener('click', function () { // 當按鈕被點擊後纔去加載 show.js 文件,文件加載成功後執行文件導出的函數 import(/* webpackChunkName: "show" */ './show').then((show) => { show('Webpack'); }) });
show.js
文件內容以下:
module.exports = function (content) { window.alert('Hello ' + content); };
代碼中最關鍵的一句是 import(/* webpackChunkName: "show" */ './show')
,Webpack 內置了對 import(*)
語句的支持,當 Webpack 遇到了相似的語句時會這樣處理:
./show.js
爲入口新生成一個 Chunk;import
所在語句時纔會去加載由 Chunk 對應生成的文件。import
返回一個 Promise,當文件加載成功時能夠在 Promise 的 then
方法中獲取到 show.js
導出的內容。在使用import()
分割代碼後,你的瀏覽器而且要支持 Promise API 才能讓代碼正常運行, 由於 import() 返回一個 Promise,它依賴 Promise。對於不原生支持 Promise 的瀏覽器,你能夠注入 Promise polyfill。
/* webpackChunkName: "show" */
的含義是爲動態生成的 Chunk 賦予一個名稱,以方便咱們追蹤和調試代碼。 若是不指定動態生成的 Chunk 的名稱,默認名稱將會是[id].js
。/* webpackChunkName: "show" */
是在 Webpack3 中引入的新特性,在 Webpack3 以前是沒法爲動態生成的 Chunk 賦予名稱的。
爲了正確的輸出在 / webpackChunkName: "show" / 中配置的 ChunkName,還須要配置下 Webpack,配置以下:
module.exports = { // JS 執行入口文件 entry: { main: './main.js', }, output: { // 爲從 entry 中配置生成的 Chunk 配置輸出文件的名稱 filename: '[name].js', // 爲動態加載的 Chunk 配置輸出文件的名稱 chunkFilename: '[name].js', } };
其中最關鍵的一行是 chunkFilename: '[name].js'
,,它專門指定動態生成的 Chunk 在輸出時的文件名稱。 若是沒有這行,分割出的代碼的文件名稱將會是 [id].js
。
在實戰中,不可能會有上面那麼簡單的場景,接下來舉一個實戰中的例子:對採用了 ReactRouter 的應用進行按需加載優化。 這個例子由一個單頁應用構成,這個單頁應用由兩個子頁面構成,經過 ReactRouter 在兩個子頁面之間切換和管理路由。
這個單頁應用的入口文件 main.js
以下:
<p data-height="565" data-theme-id="0" data-slug-hash="KROoWV" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="main.js" class="codepen">See the Pen main.js by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
以上代碼中最關鍵的部分是 getAsyncComponent
函數,它的做用是配合 ReactRouter 去按需加載組件,具體含義請看代碼中的註釋。
因爲以上源碼須要經過 Babel 去轉換後才能在瀏覽器中正常運行,須要在 Webpack 中配置好對應的 babel-loader
,源碼先交給 babel-loader
處理後再交給 Webpack 去處理其中的 import(*)
語句。 但這樣作後你很快會發現一個問題:Babel 報出錯誤說不認識 import(*)
語法。 致使這個問題的緣由是 import(*)
語法尚未被加入到在使用ES6語言中提到的 ECMAScript 標準中去, 爲此咱們須要安裝一個 Babel 插件 babel-plugin-syntax-dynamic-import
,而且將其加入到 .babelrc
中去:
{ "presets": [ "env", "react" ], "plugins": [ "syntax-dynamic-import" ] }
執行 Webpack 構建後,你會發現輸出了三個文件:
main.js
:執行入口所在的代碼塊,同時還包括 PageHome
所需的代碼,由於用戶首次打開網頁時就須要看到 PageHome
的內容,因此不對其進行按需加載,以下降用戶能感知到的加載時間;page-about.js
:當用戶訪問 /about
時纔會加載的代碼塊;page-login.js
:當用戶訪問 /login
時纔會加載的代碼塊。同時你還會發現 page-about.js
和 page-login.js
這兩個文件在首頁是不會加載的,而是會當你切換到了對應的子頁面後文件纔會開始加載。
在前面的優化方法中提到了代碼壓縮和分塊,這些都是在網絡加載層面的優化,除此以外還能夠優化代碼在運行時的效率,Prepack 就是爲此而生。
Prepack 由 Facebook 開源,它採用較爲激進的方法:在保持運行結果一致的狀況下,改變源代碼的運行邏輯,輸出性能更高的 JavaScript 代碼。 實際上 Prepack 就是一個部分求值器,編譯代碼時提早將計算結果放到編譯後的代碼中,而不是在代碼運行時纔去求值。
以以下源碼爲例:
import React, {Component} from 'react'; import {renderToString} from 'react-dom/server'; function hello(name) { return 'hello ' + name; } class Button extends Component { render() { return hello(this.props.name); } } console.log(renderToString(<Button name='webpack'/>));
被 Prepack 轉化後居然直接輸出以下:
console.log("hello webpack");
能夠看出 Prepack 經過在編譯階段預先執行了源碼獲得執行結果,再直接把運行結果輸出來以提高性能。
Prepack 的工做原理和流程大體以下:
從表面上看去這彷佛很是美好,但實際上 Prepack 還不夠成熟與完善。Prepack 目前還處於初期的開發階段,侷限性也很大,例如:
總之,如今把 Prepack 用於線上環境還爲時過早。
Prepack 須要在 Webpack 輸出最終的代碼以前,對這些代碼進行優化,就像 UglifyJS 那樣。 所以須要經過新接入一個插件來爲 Webpack 接入 Prepack,幸運的是社區中已經有人作好了這個插件:prepack-webpack-plugin。
接入該插件很是簡單,相關配置代碼以下:
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default; module.exports = { plugins: [ new PrepackWebpackPlugin() ] };
從新執行構建你就會看到輸出的被 Prepack 優化後的代碼。
Scope Hoisting 可讓 Webpack 打包出來的代碼文件更小、運行的更快, 它又譯做 "做用域提高",是在 Webpack3 中新推出的功能。 單從名字上看不出 Scope Hoisting 到底作了什麼,下面來詳細介紹它。
讓咱們先來看看在沒有 Scope Hoisting 以前 Webpack 的打包方式。
假如如今有兩個文件分別是 util.js
:
export default 'Hello,Webpack';
和入口文件 main.js
:
import str from './util.js'; console.log(str);
以上源碼用 Webpack 打包後輸出中的部分代碼以下:
[ (function (module, __webpack_exports__, __webpack_require__) { var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1); console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]); }), (function (module, __webpack_exports__, __webpack_require__) { __webpack_exports__["a"] = ('Hello,Webpack'); }) ]
在開啓 Scope Hoisting 後,一樣的源碼輸出的部分代碼以下:
[ (function (module, __webpack_exports__, __webpack_require__) { var util = ('Hello,Webpack'); console.log(util); }) ]
從中能夠看出開啓 Scope Hoisting 後,函數申明由兩個變成了一個,util.js
中定義的內容被直接注入到了 main.js
對應的模塊中。 這樣作的好處是:
Scope Hoisting 的實現原理其實很簡單:分析出模塊之間的依賴關係,儘量的把打散的模塊合併到一個函數中去,但前提是不能形成代碼冗餘。 所以只有那些被引用了一次的模塊才能被合併。
因爲 Scope Hoisting 須要分析出模塊之間的依賴關係,所以源碼必須採用 ES6 模塊化語句,否則它將沒法生效。
要在 Webpack 中使用 Scope Hoisting 很是簡單,由於這是 Webpack 內置的功能,只須要配置一個插件,相關代碼以下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); module.exports = { plugins: [ // 開啓 Scope Hoisting new ModuleConcatenationPlugin(), ], };
同時,考慮到 Scope Hoisting 依賴源碼需採用 ES6 模塊化語法,還須要配置 mainFields。 緣由在 4-10 使用 TreeShaking 中提到過:由於大部分 Npm 中的第三方庫採用了 CommonJS 語法,但部分庫會同時提供 ES6 模塊化的代碼,爲了充分發揮 Scope Hoisting 的做用,須要增長如下配置:
module.exports = { resolve: { // 針對 Npm 中的第三方模塊優先採用 jsnext:main 中指向的 ES6 模塊化語法的文件 mainFields: ['jsnext:main', 'browser', 'main'] }, };
對於採用了非 ES6 模塊化語法的代碼,Webpack 會降級處理不使用 Scope Hoisting 優化,爲了知道 Webpack 對哪些代碼作了降級處理, 你能夠在啓動 Webpack 時帶上 --display-optimization-bailout
參數,這樣在輸出日誌中就會包含相似以下的日誌:
[0] ./main.js + 1 modules 80 bytes {0} [built] ModuleConcatenation bailout: Module is not an ECMAScript module
其中的 ModuleConcatenation bailout
告訴了你哪一個文件由於什麼緣由致使了降級處理。
也就是說要開啓 Scope Hoisting 併發揮最大做用的配置以下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin'); module.exports = { resolve: { // 針對 Npm 中的第三方模塊優先採用 jsnext:main 中指向的 ES6 模塊化語法的文件 mainFields: ['jsnext:main', 'browser', 'main'] }, plugins: [ // 開啓 Scope Hoisting new ModuleConcatenationPlugin(), ], };
前面雖然介紹了很是多的優化方法,但這些方法也沒法涵蓋全部的場景,爲此你須要對輸出結果作分析,以決定下一步的優化方向。
最直接的分析方法就是去閱讀 Webpack 輸出的代碼,但因爲 Webpack 輸出的代碼可讀性很是差並且文件很是大,這會讓你很是頭疼。 爲了更簡單直觀的分析輸出結果,社區中出現了許多可視化的分析工具。這些工具以圖形的方式把結果更加直觀的展現出來,讓你快速看到問題所在。 接下來教你如何使用這些工具。
在啓動 Webpack 時,支持兩個參數,分別是:
--profile
:記錄下構建過程當中的耗時信息;--json
:以 JSON 的格式輸出構建結果,最後只輸出一個 .json
文件,這個文件中包括全部構建相關的信息。在啓動 Webpack 時帶上以上兩個參數,啓動命令以下 webpack --profile --json > stats.json
,你會發現項目中多出了一個 stats.json
文件。 這個 stats.json
文件是給後面介紹的可視化分析工具使用的。
webpack --profile --json
會輸出字符串形式的 JSON,> stats.json
是 UNIX/Linux 系統中的管道命令、含義是把webpack --profile --json
輸出的內容經過管道輸出到stats.json
文件中。
Webpack 官方提供了一個可視化分析工具 Webpack Analyse,它是一個在線 Web 應用。
打開 Webpack Analyse 連接的網頁後,你就會看到一個彈窗提示你上傳 JSON 文件,也就是須要上傳上面講到的 stats.json
文件,如圖:
Webpack Analyse 不會把你選擇的 stats.json
文件發達到服務器,而是在瀏覽器本地解析,你不用擔憂本身的代碼爲此而泄露。 選擇文件後,你立刻就能以下的效果圖:
它分爲了六大板塊,分別是:
Modules
:展現全部的模塊,每一個模塊對應一個文件。而且還包含全部模塊之間的依賴關係圖、模塊路徑、模塊ID、模塊所屬 Chunk、模塊大小;Chunks
:展現全部的代碼塊,一個代碼塊中包含多個模塊。而且還包含代碼塊的ID、名稱、大小、每一個代碼塊包含的模塊數量,以及代碼塊之間的依賴關係圖;Assets
:展現全部輸出的文件資源,包括 .js
、.css
、圖片等。而且還包括文件名稱、大小、該文件來自哪一個代碼塊;Warnings
:展現構建過程當中出現的全部警告信息;Errors
:展現構建過程當中出現的全部錯誤信息;Hints
:展現處理每一個模塊的過程當中的耗時。下面以在 3-10管理多個單頁應用 中使用的項目爲例,來分析其 stats.json
文件。
點擊 Modules,查看模塊信息,效果圖以下:
因爲依賴了大量第三方模塊,文件數量大,致使模塊之間的依賴關係圖太密集而沒法看清,但你能夠進一步放大查看。
點擊 Chunks,查看代碼塊信息,效果圖以下:
由代碼塊之間的依賴關係圖能夠看出兩個頁面級的代碼塊login
和index
依賴提取出來的公共代碼塊 common。
點擊 Assets,查看輸出的文件資源,效果圖以下:
點擊 Hints,查看輸出過程當中的耗時分佈,效果圖以下:
從 Hints 能夠看出每一個文件在處理過程的開始時間和結束時間,從而能夠找出是哪一個文件致使構建緩慢。
webpack-bundle-analyzer
webpack-bundle-analyzer 是另外一個可視化分析工具, 它雖然沒有官方那樣有那麼多功能,但比官方的要更加直觀。
先來看下它的效果圖:
它能方便的讓你知道:
接入 webpack-bundle-analyzer
的方法很簡單,步驟以下:
npm i -g webpack-bundle-analyzer
;stats.json
文件;webpack-bundle-analyzer
後,瀏覽器會打開對應網頁看到以上效果。本章從開發體驗和輸出質量兩個角度講解了如何優化項目中的 Webpack 配置,這些優化的方法都是來自項目實戰中的經驗積累。 雖然每一小節都是一個個獨立的優化方法,可是有些優化方法並不衝突能夠相互組合,以達到最佳的效果。
如下將給出是結合了本章全部優化方法的實例項目,因爲構建速度和輸出質量不能兼得,按照開發環境和線上環境爲該項目配置了兩份文件,分別以下:
側重優化開發體驗的配置文件 webpack.config.js
:
<p data-height="565" data-theme-id="0" data-slug-hash="pVMVgW" data-default-tab="js" data-user="whjin" data-embed-version="2" data-pen-title="webpack-dist.config.js" class="codepen">See the Pen webpack-dist.config.js by whjin (@whjin) on CodePen.</p>
<script async src="https://static.codepen.io/ass...;></script>
本章介紹的優化方法雖然難以涵蓋 Webpack 的方方面面,但足以解決實戰中常見的場景。 對於本書沒有介紹到的場景,你須要根據本身的需求按照如下思路去優化:
同時你還須要跟緊社區的迭代,學習他人的優化方法,瞭解最新的 Webpack 特性和新涌現出的插件、Loader。