webpack構建優化:縮小檔案搜尋範圍

ldlecit發表於2018-10-30

Webpack 啟動後會從配置的 Entry 出發,解析出檔案中的匯入語句,再遞迴的解析。 在遇到匯入語句時 Webpack 會做兩件事情:

  1. 根據匯入語句去尋找對應的要匯入的檔案。例如 require('react') 匯入語句對應的檔案是 ./node_modules/react/react.jsrequire('./util') 對應的檔案是 ./util.js
  2. 根據找到的要匯入檔案的字尾,使用配置中的 Loader 去處理檔案。例如使用 ES6 開發的 JavaScript 檔案需要使用 babel-loader 去處理。

以上兩件事情雖然對於處理一個檔案非常快,但是當專案大了以後檔案量會變的非常多,這時候構建速度慢的問題就會暴露出來。 雖然以上兩件事情無法避免,但需要儘量減少以上兩件事情的發生,以提高速度。

接下來一一介紹可以優化它們的途徑。

優化 loader 配置

由於 Loader 對檔案的轉換操作很耗時,需要讓儘可能少的檔案被 Loader 處理。

在使用 Loader 時可以通過 testincludeexclude 三個配置項來命中 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.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.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 配置有關係,對應關係如下:

  • targetweb 或者 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 庫中包含兩套程式碼:

  • 一套是採用 CommonJS 規範的模組化程式碼,這些檔案都放在 lib 目錄下,以 package.json 中指定的入口檔案 react.js 為模組的入口。
  • 一套是把 React 所有相關的程式碼打包好的完整程式碼放到一個單獨的檔案中,這些程式碼沒有采用模組化可以直接執行。其中 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'),
    }
  },
};
複製程式碼


優化 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$/],
  },
};
複製程式碼

注意被忽略掉的檔案裡不應該包含 importrequiredefine 等模組化語句,不然會導致構建出的程式碼中包含無法在瀏覽器環境下執行的模組化語句。

以上就是所有和縮小檔案搜尋範圍相關的構建效能優化了,在根據自己專案的需要去按照以上方法改造後,你的構建速度一定會有所提升。

轉載:http://webpack.wuhaolin.cn/4%E4%BC%98%E5%8C%96/4-1%E7%BC%A9%E5%B0%8F%E6%96%87%E4%BB%B6%E6%90%9C%E7%B4%A2%E8%8C%83%E5%9B%B4.html



相關文章