Webpack 效能優化 (一)(使用別名做重定向)

OneAPM官方技術部落格發表於2015-07-09

前言

Webpack 是 OneAPM 前端技術棧中很重要的一部分,它非常好用,如果你還不瞭解它,建議你閱讀這篇 Webpack 入門指迷 ,在 OneAPM 我們用它完成靜態資源打包,ES6 程式碼的轉換 ,React 元件的組織等,在接下來的日子裡,我們將通過一系列文章和業界分享我們在使用 Webpack 過程中關於效能方面的經驗。

作為系列文章的第一篇,我們會重點介紹 Webpack 中的 resolve.alias ,也就是請求重定向。不過請注意 Webpack 裡的請求是對模組的依賴,也就是一個 require語句,而不是一個 HTTP 請求。

必要的準備

  • 需要你有一定的 Node.js 基礎
  • 電腦上裝有最新版的 Webpack (npm install webpack -g)
  • 瞭解 Webpack 配置檔案的格式

例子:本地時鐘

要實現的功能很簡單,就是在頁面上用中文顯示當前時間,需要用到 moment 這個庫,這個庫封裝了很多和日期相關的函式,而且自帶了國際化的支援。

新建一個 Node.js 專案

使用 npm init 初始化你的專案,然後通過npm install moment -D加上 moment 的開發者依賴。

新建一個entry.js作為入口檔案,當然你也可以用 app.js 這樣的名字,只是大部分的 Webpack 示例都是用的是 entry.js

var moment = require('moment');
document.write(moment().locale('zh-cn').format('LLLL'));

新建一個頁面index.html, 引用 bundle.js:

<body>
<h5>當前時間:</h5>
<script src="dist/bundle.js"></script>
</body>

此時的檔案目錄看起來是這樣的:

index.html
package.json
entry.js
node_modules/moment

到目前為止 bundle.js 這個檔案還不存在,不過彆著急,接下來的工作就交給 Webpack 來完成。

index.html  ------------------------+               
package.json                        |               
                                    +--> <Clock App>
entry.js    --------+               |               
                    +-->bundle.js+--+               
node_modules/moment-+                                                                                                      

如圖,Webpack 會把 entry.jsmoment模組一起打包成一個 bundle.js 檔案,和 index.html 一起構成了我們的 Clock App。怎麼樣,是不是已經聽到 Clock App 滴答作響了?

使用 webpack 打包程式碼

在命令列執行:

webpack --entry ./entry.js --output-path dist --output-file bundle.js

你會看到類似下面的輸出結果:

Hash: bf9007fb1e0cb30e3ef7
Version: webpack 1.10.0
Time: 650ms
    Asset    Size  Chunks             Chunk Names
bundle.js  378 kB       0  [emitted]  null
   [0] ./entry.js 125 bytes {0} [built]
    + 86 hidden modules

可以看到,耗時 650ms,這麼慢著實讓人意外,一定要想辦法提高“新一代神器”速度;另一方面,最後一行的 + 86 hidden modules 非常讓人懷疑:明明是一個簡單的 Clock App,怎麼會有這麼多的依賴。

如何快速定位 Webpack 速度慢的原因

再一次,在命令列輸入:

webpack --entry ./entry.js --output-path dist --output-file bundle.js \
--colors \
--profile \
--display-modules

不過這次新增加了三個引數,這三個引數的含義分別是:

  • --colors 輸出結果帶彩色,比如:會用紅色顯示耗時較長的步驟
  • --profile輸出效能資料,可以看到每一步的耗時
  • --display-modules預設情況下 node_modules 下的模組會被隱藏,加上這個引數可以顯示這些被隱藏的模組 這次命令列的結果已經很有參考價值,可以幫助我們定位耗時比較長的步驟

    Hash: bf9007fb1e0cb30e3ef7
    Version: webpack 1.10.0
    Time: 650ms
    Asset    Size  Chunks             Chunk Names
    bundle.js  378 kB       0  [emitted]  null
       [0] ./entry.js 125 bytes {0} [built]
           factory:11ms building:8ms = 19ms
       [1] ../~/moment/moment.js 102 kB {0} [built]
           [0] 19ms -> factory:7ms building:141ms = 167ms
       [2] (webpack)/buildin/module.js 251 bytes {0} [built]
           [0] 19ms -> [1] 148ms -> factory:132ms building:159ms = 458ms
       [3] ../~/moment/locale ^\.\/.*$ 2.01 kB {0} [optional] [built]
           [0] 19ms -> [1] 148ms -> factory:6ms building:10ms dependencies:113ms = 296ms
       [4] ../~/moment/locale/af.js 2.57 kB {0} [optional] [built]
           [0] 19ms -> [1] 148ms -> [3] 16ms -> factory:52ms building:65ms dependencies:138ms =      438ms
                       ..... 廣告分割線,Node.js 工程師簡歷請發 nodejs@oneapm.com ......
       [85] ../~/moment/locale/zh-cn.js 4.31 kB {0} [optional] [built]
            [0] 22ms -> [1] 162ms -> [3] 18ms -> factory:125ms building:145ms dependencies:22ms    =  494ms
       [86] ../~/moment/locale/zh-tw.js 3.07 kB {0} [optional] [built]
            [0] 22ms -> [1] 162ms -> [3] 18ms -> factory:126ms building:146ms dependencies:21ms  = 495ms
    

從命令列的結果裡可以看到從 Request[4] 到 Request[86] 都是在解析 moment.js附帶的大量本地化檔案。所以我們遇到的速度慢的問題其實是由 moment引起的。

如果你想知道為什麼 Webpack 會載入這麼多的模組,可以參考這篇文章 Why Enormous Locales During Webpack MomentJS

我們再來看看 entry.js程式碼的第一行,標準的 CommonJS寫法:

var moment = require('moment');

也就是說,請求的是 moment的原始碼。實際上,通過 NPM 安裝moment 的時候會同時安裝 moment 的原始碼和壓縮後的程式碼,試驗證明下面這種寫法也是可行的:

var moment = require('moment/min/moment-with-locales.min.js');

只不過這樣改,可讀性會有所下降,而且每一個用到moment 的地方都得這麼寫。另外,如果同樣的問題出現在第三方模組中,修改別人程式碼就不那麼方便了。下面來看看用 Webpack 怎麼解決這個問題。

在 Webpack 中使用別名

別名(resolve.alias) 是 Webpack 的一個配置項,它的作用是把使用者的一個請求重定向到另一個路徑,例如通過修改 webpack.config.js配置檔案,加入:

  resolve: {
    alias: {
        moment: "moment/min/moment-with-locales.min.js"
    }
  }

這樣待打包的指令碼中的 require('moment'); 其實就等價於 require('moment/min/moment-with-locales.min.js'); 。通過別名的使用在本例中可以減少幾乎一半的時間。

Hash: cdea65709b783ee0741a
Version: webpack 1.10.0
Time: 320ms
     Asset    Size  Chunks             Chunk Names
bundle.js  148 kB       0  [emitted]  main
   [0] ./entry.js 125 bytes {0} [built]
       factory:11ms building:9ms = 20ms
   [1] ../~/moment/min/moment-with-locales.min.js 146 kB {0} [built] [1 warning]
       [0] 20ms -> factory:8ms building:263ms = 291ms
   [2] (webpack)/buildin/module.js 251 bytes {0} [built]
       [0] 20ms -> [1] 271ms -> factory:3ms building:1ms = 295ms

WARNING in ../~/moment/min/moment-with-locales.min.js
Module not found: Error: Cannot resolve 'file' or 'directory' ./locale in    */webpack_performance/node_modules/moment/min
 @ ../~/moment/min/moment-with-locales.min.js 1:2731-2753

Webpack中忽略對已知檔案的解析

module.noParsewebpack 的另一個很有用的配置項,如果你 確定一個模組中沒有其它新的依賴 就可以配置這項,webpack 將不再掃描這個檔案中的依賴。

  module: {
    noParse: [/moment-with-locales/]
  }

這樣修改,再結合前面重新命名的例子,更新後的流程是:

  • webpack 檢查到 entry.js 檔案對 moment的請求;
  • 請求被 alias 重定向,轉而請求 moment/min/moment-with-locales.min.js;
  • noParse 規則中的 /moment-with-locales/一條生效,所以 webpack 就直接把依賴打包進了 bundle.js

    Hash: 907880ed7638b4ed70b9 Version: webpack 1.10.0 Time: 76ms Asset Size Chunks Chunk Names bundle.js 147 kB 0 [emitted] main [0] ./entry.js 125 bytes {0} [built] factory:13ms building:13ms = 26ms [1] ../~/moment/min/moment-with-locales.min.js 146 kB {0} [built] [0] 26ms -> factory:13ms building:5ms = 44ms

時間進一步被壓縮,只需要 76ms,比前一步還減少了 75%。

在 Webpack 中使用公用 CDN

Webpack 是如此的強大,用其打包的指令碼可以執行在多種環境下,Web 環境只是其預設的一種,也是最常用的一種。考慮到 Web 上有很多的公用 CDN 服務,那麼 怎麼將 Webpack 和公用的 CDN 結合使用呢?方法是使用 externals宣告一個外部依賴。

  externals: {
    moment: true
  }

當然了 HTML 程式碼裡需要加上一行

<script src="//apps.bdimg.com/libs/moment/2.8.3/moment-with-locales.min.js"></script>

這次打包,結果只用了 49 ms,幾乎達到了極限。

總結

本文結合本地時鐘的例子,展示了定位 Webpack 效能問題的步驟,以及所需要的兩個引數 :--display-modules--profile。然後,重點介紹了 resolve.alias 即利用別名做重定向的方法和場景,在此基礎上,配合module.noParse 忽略某些模組的解析可以進一步加快速度。最後介紹了用 externals 定義外部依賴方法來使用公用 CDN。

關於

本文相關的原始碼在: https://github.com/wyvernnot/webpack_performance/tree/master/moment-example;


本文系OneAPM工程師原創文章。OneAPM是中國基礎軟體領域的新興領軍企業,能幫助企業使用者和開發者輕鬆實現:緩慢的程式程式碼和SQL語句的實時抓取。想閱讀更多技術文章,請訪問OneAPM官方技術部落格

相關文章