摘要:使用要給項(xiàng)目構(gòu)建接入動(dòng)態(tài)鏈接庫(kù)的思想,需要完成以下事情把網(wǎng)頁(yè)依賴的基礎(chǔ)模塊抽離出來(lái),打包到一個(gè)個(gè)多帶帶的動(dòng)態(tài)鏈接庫(kù)中去。接入已經(jīng)內(nèi)置了對(duì)動(dòng)態(tài)鏈接庫(kù)的支持,需要通過(guò)個(gè)內(nèi)置的插件接入,它們分別是插件用于打包出一個(gè)個(gè)多帶帶的動(dòng)態(tài)鏈接庫(kù)文件。
webpack優(yōu)化
查看所有文檔頁(yè)面:全棧開(kāi)發(fā),獲取更多信息。優(yōu)化開(kāi)發(fā)體驗(yàn)原文鏈接:webpack優(yōu)化,原文廣告模態(tài)框遮擋,閱讀體驗(yàn)不好,所以整理成本文,方便查找。
優(yōu)化構(gòu)建速度。在項(xiàng)目龐大時(shí)構(gòu)建耗時(shí)可能會(huì)變的很長(zhǎng),每次等待構(gòu)建的耗時(shí)加起來(lái)也會(huì)是個(gè)大數(shù)目。
縮小文件搜索范圍
使用 DllPlugin
使用 HappyPack
使用 ParallelUglifyPlugin
優(yōu)化使用體驗(yàn)。通過(guò)自動(dòng)化手段完成一些重復(fù)的工作,讓我們專注于解決問(wèn)題本身。
使用自動(dòng)刷新
開(kāi)啟模塊熱替換
優(yōu)化輸出質(zhì)量優(yōu)化輸出質(zhì)量的目的是為了給用戶呈現(xiàn)體驗(yàn)更好的網(wǎng)頁(yè),例如減少首屏加載時(shí)間、提升性能流暢度等。 這至關(guān)重要,因?yàn)樵诨ヂ?lián)網(wǎng)行業(yè)競(jìng)爭(zhēng)日益激烈的今天,這可能關(guān)系到你的產(chǎn)品的生死。
優(yōu)化輸出質(zhì)量本質(zhì)是優(yōu)化構(gòu)建輸出的要發(fā)布到線上的代碼,分為以下幾點(diǎn):
減少用戶能感知到的加載時(shí)間,也就是首屏加載時(shí)間。
區(qū)分環(huán)境
壓縮代碼
CDN 加速
使用 Tree Shaking
提取公共代碼
按需加載
提升流暢度,也就是提升代碼性能。
使用 Prepack
開(kāi)啟 Scope Hoisting
縮小文件搜索范圍Webpack 啟動(dòng)后會(huì)從配置的 Entry 出發(fā),解析出文件中的導(dǎo)入語(yǔ)句,再遞歸的解析。 在遇到導(dǎo)入語(yǔ)句時(shí) Webpack 會(huì)做兩件事情:
根據(jù)導(dǎo)入語(yǔ)句去尋找對(duì)應(yīng)的要導(dǎo)入的文件。例如 require("react") 導(dǎo)入語(yǔ)句對(duì)應(yīng)的文件是 ./node_modules/react/react.js,require("./util") 對(duì)應(yīng)的文件是 ./util.js。
根據(jù)找到的要導(dǎo)入文件的后綴,使用配置中的 Loader 去處理文件。例如使用 ES6 開(kāi)發(fā)的 JavaScript 文件需要使用 babel-loader 去處理。
優(yōu)化 loader 配置由于 Loader 對(duì)文件的轉(zhuǎn)換操作很耗時(shí),需要讓盡可能少的文件被 Loader 處理。
在 Module 中介紹過(guò)在使用 Loader 時(shí)可以通過(guò) test 、 include 、 exclude 三個(gè)配置項(xiàng)來(lái)命中 Loader 要應(yīng)用規(guī)則的文件。 為了盡可能少的讓文件被 Loader 處理,可以通過(guò) include 去命中只有哪些文件需要被處理。
以采用 ES6 的項(xiàng)目為例,在配置 babel-loader 時(shí),可以這樣:
module.exports = { module: { rules: [ { // 如果項(xiàng)目源碼中只有 js 文件就不要寫(xiě)成 /.jsx?$/,提升正則表達(dá)式性能 test: /.js$/, // babel-loader 支持緩存轉(zhuǎn)換出的結(jié)果,通過(guò) cacheDirectory 選項(xiàng)開(kāi)啟 use: ["babel-loader?cacheDirectory"], // 只對(duì)項(xiàng)目根目錄下的 src 目錄中的文件采用 babel-loader include: path.resolve(__dirname, "src"), }, ] }, };
你可以適當(dāng)?shù)恼{(diào)整項(xiàng)目的目錄結(jié)構(gòu),以方便在配置 Loader 時(shí)通過(guò) include 去縮小命中范圍。優(yōu)化 resolve.modules 配置
在 Resolve 中介紹過(guò) resolve.modules 用于配置 Webpack 去哪些目錄下尋找第三方模塊。
resolve.modules 的默認(rèn)值是 ["node_modules"],含義是先去當(dāng)前目錄下的 ./node_modules 目錄下去找想找的模塊,如果沒(méi)找到就去上一級(jí)目錄 ../node_modules 中找,再?zèng)]有就去 ../../node_modules 中找,以此類推,這和 Node.js 的模塊尋找機(jī)制很相似。
當(dāng)安裝的第三方模塊都放在項(xiàng)目根目錄下的 ./node_modules 目錄下時(shí),沒(méi)有必要按照默認(rèn)的方式去一層層的尋找,可以指明存放第三方模塊的絕對(duì)路徑,以減少尋找,配置如下:
module.exports = { resolve: { // 使用絕對(duì)路徑指明第三方模塊存放的位置,以減少搜索步驟 // 其中 __dirname 表示當(dāng)前工作目錄,也就是項(xiàng)目根目錄 modules: [path.resolve(__dirname, "node_modules")] }, };優(yōu)化 resolve.mainFields 配置
在 Resolve 中介紹過(guò) resolve.mainFields 用于配置第三方模塊使用哪個(gè)入口文件。
安裝的第三方模塊中都會(huì)有一個(gè) package.json 文件用于描述這個(gè)模塊的屬性,其中有些字段用于描述入口文件在哪里,resolve.mainFields 用于配置采用哪個(gè)字段作為入口文件的描述。
可以存在多個(gè)字段描述入口文件的原因是因?yàn)橛行┠K可以同時(shí)用在多個(gè)環(huán)境中,準(zhǔn)對(duì)不同的運(yùn)行環(huán)境需要使用不同的代碼。 以 isomorphic-fetch 為例,它是 fetch API 的一個(gè)實(shí)現(xiàn),但可同時(shí)用于瀏覽器和 Node.js 環(huán)境。 它的 package.json 中就有2個(gè)入口文件描述字段:
{ "browser": "fetch-npm-browserify.js", "main": "fetch-npm-node.js" }
isomorphic-fetch 在不同的運(yùn)行環(huán)境下使用不同的代碼是因?yàn)?fetch API 的實(shí)現(xiàn)機(jī)制不一樣,在瀏覽器中通過(guò)原生的 fetch 或者 XMLHttpRequest 實(shí)現(xiàn),在 Node.js 中通過(guò) http 模塊實(shí)現(xiàn)。
resolve.mainFields 的默認(rèn)值和當(dāng)前的 target 配置有關(guān)系,對(duì)應(yīng)關(guān)系如下:
當(dāng) target 為 web 或者 webworker 時(shí),值是 ["browser", "module", "main"]
當(dāng) target 為其它情況時(shí),值是 ["module", "main"]
以 target 等于 web 為例,Webpack 會(huì)先采用第三方模塊中的 browser 字段去尋找模塊的入口文件,如果不存在就采用 module 字段,以此類推。
為了減少搜索步驟,在你明確第三方模塊的入口文件描述字段時(shí),你可以把它設(shè)置的盡量少。 由于大多數(shù)第三方模塊都采用 main 字段去描述入口文件的位置,可以這樣配置 Webpack:
module.exports = { resolve: { // 只采用 main 字段作為入口文件描述字段,以減少搜索步驟 mainFields: ["main"], }, };
使用本方法優(yōu)化時(shí),你需要考慮到所有運(yùn)行時(shí)依賴的第三方模塊的入口文件描述字段,就算有一個(gè)模塊搞錯(cuò)了都可能會(huì)造成構(gòu)建出的代碼無(wú)法正常運(yùn)行。優(yōu)化 resolve.alias 配置
resolve.alias 配置項(xiàng)通過(guò)別名來(lái)把原導(dǎo)入路徑映射成一個(gè)新的導(dǎo)入路徑。
在實(shí)戰(zhàn)項(xiàng)目中經(jīng)常會(huì)依賴一些龐大的第三方模塊,以 React 庫(kù)為例,安裝到 node_modules 目錄下的 React 庫(kù)的目錄結(jié)構(gòu)如下:
├── dist │ ├── react.js │ └── react.min.js ├── lib │ ... 還有幾十個(gè)文件被忽略 │ ├── LinkedStateMixin.js │ ├── createClass.js │ └── React.js ├── package.json └── react.js
可以看到發(fā)布出去的 React 庫(kù)中包含兩套代碼:
一套是采用 CommonJS 規(guī)范的模塊化代碼,這些文件都放在 lib 目錄下,以 package.json 中指定的入口文件 react.js 為模塊的入口。
一套是把 React 所有相關(guān)的代碼打包好的完整代碼放到一個(gè)多帶帶的文件中,這些代碼沒(méi)有采用模塊化可以直接執(zhí)行。其中 dist/react.js 是用于開(kāi)發(fā)環(huán)境,里面包含檢查和警告的代碼。dist/react.min.js 是用于線上環(huán)境,被最小化了。
默認(rèn)情況下 Webpack 會(huì)從入口文件 ./node_modules/react/react.js 開(kāi)始遞歸的解析和處理依賴的幾十個(gè)文件,這會(huì)時(shí)一個(gè)耗時(shí)的操作。 通過(guò)配置 resolve.alias 可以讓 Webpack 在處理 React 庫(kù)時(shí),直接使用多帶帶完整的 react.min.js 文件,從而跳過(guò)耗時(shí)的遞歸解析操作。
相關(guān) Webpack 配置如下:
module.exports = { resolve: { // 使用 alias 把導(dǎo)入 react 的語(yǔ)句換成直接使用多帶帶完整的 react.min.js 文件, // 減少耗時(shí)的遞歸解析操作 alias: { "react": path.resolve(__dirname, "./node_modules/react/dist/react.min.js"), } }, };
優(yōu)化 resolve.extensions 配置除了 React 庫(kù)外,大多數(shù)庫(kù)發(fā)布到 Npm 倉(cāng)庫(kù)中時(shí)都會(huì)包含打包好的完整文件,對(duì)于這些庫(kù)你也可以對(duì)它們配置 alias。
但是對(duì)于有些庫(kù)使用本優(yōu)化方法后會(huì)影響到后面要講的使用 Tree-Shaking 去除無(wú)效代碼的優(yōu)化,因?yàn)榇虬玫耐暾募杏胁糠执a你的項(xiàng)目可能永遠(yuǎn)用不上。 一般對(duì)整體性比較強(qiáng)的庫(kù)采用本方法優(yōu)化,因?yàn)橥暾募械拇a是一個(gè)整體,每一行都是不可或缺的。 但是對(duì)于一些工具類的庫(kù),例如 lodash,你的項(xiàng)目可能只用到了其中幾個(gè)工具函數(shù),你就不能使用本方法去優(yōu)化,因?yàn)檫@會(huì)導(dǎo)致你的輸出代碼中包含很多永遠(yuǎn)不會(huì)執(zhí)行的代碼。
在導(dǎo)入語(yǔ)句沒(méi)帶文件后綴時(shí),Webpack 會(huì)自動(dòng)帶上后綴后去嘗試詢問(wèn)文件是否存在。resolve.extensions 用于配置在嘗試過(guò)程中用到的后綴列表,默認(rèn)是:
extensions: [".js", ".json"]
也就是說(shuō)當(dāng)遇到 require("./data") 這樣的導(dǎo)入語(yǔ)句時(shí),Webpack 會(huì)先去尋找 ./data.js 文件,如果該文件不存在就去尋找 ./data.json 文件,如果還是找不到就報(bào)錯(cuò)。
如果這個(gè)列表越長(zhǎng),或者正確的后綴在越后面,就會(huì)造成嘗試的次數(shù)越多,所以 resolve.extensions 的配置也會(huì)影響到構(gòu)建的性能。 在配置 resolve.extensions 時(shí)你需要遵守以下幾點(diǎn),以做到盡可能的優(yōu)化構(gòu)建性能:
后綴嘗試列表要盡可能的小,不要把項(xiàng)目中不可能存在的情況寫(xiě)到后綴嘗試列表中。
頻率出現(xiàn)最高的文件后綴要優(yōu)先放在最前面,以做到盡快的退出尋找過(guò)程。
在源碼中寫(xiě)導(dǎo)入語(yǔ)句時(shí),要盡可能的帶上后綴,從而可以避免尋找過(guò)程。例如在你確定的情況下把 require("./data") 寫(xiě)成 require("./data.json")。
相關(guān) Webpack 配置如下:
module.exports = { resolve: { // 盡可能的減少后綴嘗試的可能性 extensions: ["js"], }, };優(yōu)化 module.noParse 配置
module.noParse 配置項(xiàng)可以讓 Webpack 忽略對(duì)部分沒(méi)采用模塊化的文件的遞歸解析處理,這樣做的好處是能提高構(gòu)建性能。 原因是一些庫(kù),例如 jQuery 、ChartJS, 它們龐大又沒(méi)有采用模塊化標(biāo)準(zhǔn),讓 Webpack 去解析這些文件耗時(shí)又沒(méi)有意義。
在上面的 優(yōu)化 resolve.alias 配置 中講到多帶帶完整的 react.min.js 文件就沒(méi)有采用模塊化,讓我們來(lái)通過(guò)配置 module.noParse 忽略對(duì) react.min.js 文件的遞歸解析處理, 相關(guān) Webpack 配置如下:
const path = require("path"); module.exports = { module: { // 獨(dú)完整的 `react.min.js` 文件就沒(méi)有采用模塊化,忽略對(duì) `react.min.js` 文件的遞歸解析處理 noParse: [/react.min.js$/], }, };
注意被忽略掉的文件里不應(yīng)該包含 import 、 require 、 define 等模塊化語(yǔ)句,不然會(huì)導(dǎo)致構(gòu)建出的代碼中包含無(wú)法在瀏覽器環(huán)境下執(zhí)行的模塊化語(yǔ)句。
以上就是所有和縮小文件搜索范圍相關(guān)的構(gòu)建性能優(yōu)化了,在根據(jù)自己項(xiàng)目的需要去按照以上方法改造后,你的構(gòu)建速度一定會(huì)有所提升。
使用 DllPlugin要給 Web 項(xiàng)目構(gòu)建接入動(dòng)態(tài)鏈接庫(kù)的思想,需要完成以下事情:
把網(wǎng)頁(yè)依賴的基礎(chǔ)模塊抽離出來(lái),打包到一個(gè)個(gè)多帶帶的動(dòng)態(tài)鏈接庫(kù)中去。一個(gè)動(dòng)態(tài)鏈接庫(kù)中可以包含多個(gè)模塊。
當(dāng)需要導(dǎo)入的模塊存在于某個(gè)動(dòng)態(tài)鏈接庫(kù)中時(shí),這個(gè)模塊不能被再次被打包,而是去動(dòng)態(tài)鏈接庫(kù)中獲取。
當(dāng)需要導(dǎo)入的模塊存在于某個(gè)動(dòng)態(tài)鏈接庫(kù)中時(shí),這個(gè)模塊不能被再次被打包,而是去動(dòng)態(tài)鏈接庫(kù)中獲取。
為什么給 Web 項(xiàng)目構(gòu)建接入動(dòng)態(tài)鏈接庫(kù)的思想后,會(huì)大大提升構(gòu)建速度呢? 原因在于包含大量復(fù)用模塊的動(dòng)態(tài)鏈接庫(kù)只需要編譯一次,在之后的構(gòu)建過(guò)程中被動(dòng)態(tài)鏈接庫(kù)包含的模塊將不會(huì)在重新編譯,而是直接使用動(dòng)態(tài)鏈接庫(kù)中的代碼。 由于動(dòng)態(tài)鏈接庫(kù)中大多數(shù)包含的是常用的第三方模塊,例如 react、react-dom,只要不升級(jí)這些模塊的版本,動(dòng)態(tài)鏈接庫(kù)就不用重新編譯。
接入 WebpackWebpack 已經(jīng)內(nèi)置了對(duì)動(dòng)態(tài)鏈接庫(kù)的支持,需要通過(guò)2個(gè)內(nèi)置的插件接入,它們分別是:
DllPlugin 插件:用于打包出一個(gè)個(gè)多帶帶的動(dòng)態(tài)鏈接庫(kù)文件。
DllReferencePlugin 插件:用于在主要配置文件中去引入 DllPlugin 插件打包好的動(dòng)態(tài)鏈接庫(kù)文件。
下面以基本的 React 項(xiàng)目為例,為其接入 DllPlugin,在開(kāi)始前先來(lái)看下最終構(gòu)建出的目錄結(jié)構(gòu):
├── main.js ├── polyfill.dll.js ├── polyfill.manifest.json ├── react.dll.js └── react.manifest.json
其中包含兩個(gè)動(dòng)態(tài)鏈接庫(kù)文件,分別是:
polyfill.dll.js 里面包含項(xiàng)目所有依賴的 polyfill,例如 Promise、fetch 等 API。
react.dll.js 里面包含 React 的基礎(chǔ)運(yùn)行環(huán)境,也就是 react 和 react-dom 模塊。
以 react.dll.js 文件為例,其文件內(nèi)容大致如下:
var _dll_react = (function(modules) { // ... 此處省略 webpackBootstrap 函數(shù)代碼 }([ function(module, exports, __webpack_require__) { // 模塊 ID 為 0 的模塊對(duì)應(yīng)的代碼 }, function(module, exports, __webpack_require__) { // 模塊 ID 為 1 的模塊對(duì)應(yīng)的代碼 }, // ... 此處省略剩下的模塊對(duì)應(yīng)的代碼 ]));
可見(jiàn)一個(gè)動(dòng)態(tài)鏈接庫(kù)文件中包含了大量模塊的代碼,這些模塊存放在一個(gè)數(shù)組里,用數(shù)組的索引號(hào)作為 ID。 并且還通過(guò) _dll_react 變量把自己暴露在了全局中,也就是可以通過(guò) window._dll_react 可以訪問(wèn)到它里面包含的模塊。
其中 polyfill.manifest.json 和 react.manifest.json 文件也是由 DllPlugin 生成出,用于描述動(dòng)態(tài)鏈接庫(kù)文件中包含哪些模塊, 以 react.manifest.json 文件為例,其文件內(nèi)容大致如下:
See the Pen react.manifest.json by whjin (@whjin) on CodePen.