瞭解 CommonJS 模組如何影響應用程式的樹狀結構
在這篇文章中,我們會探討 CommonJS 是什麼,以及為什麼您的 JavaScript 檔案包變得比必要大。
摘要:為確保 Bundler 能順利最佳化應用程式,請避免採用 CommonJS 模組,並在整個應用程式中使用 ECMAScript 模組語法。
什麼是 CommonJS?
CommonJS 是 2009 年推出的標準 JavaScript 模組慣例。這項功能一開始是供網路瀏覽器以外的使用,主要用於伺服器端應用程式。
使用 CommonJS,您可以定義模組、從模組匯出功能,以及在其他模組中匯入模組。例如,以下程式碼片段定義可匯出以下五個函式的模組:add
、subtract
、multiply
、divide
和 max
:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
之後,另一個模組可以匯入及使用以下部分或所有函式:
// index.js
const { add } = require('./utils.js');
console.log(add(1, 2));
使用 node
叫用 index.js
後,控制台會輸出數字 3
。
由於 2010 年代初期,瀏覽器缺少標準化模組系統,因此 CommonJS 也成為 JavaScript 用戶端程式庫常用的模組格式。
CommonJS 對最終套件大小有何影響?
伺服器端 JavaScript 應用程式的大小對於瀏覽器而言並沒有那麼重要,所以 CommonJS 在設計上並不會考慮縮減生產套件的大小。另外,分析也顯示 JavaScript 套件大小仍然是造成瀏覽器應用程式速度變慢的關鍵原因之一。
JavaScript 組合器和壓縮器 (例如 webpack
和 terser
) 會執行不同的最佳化作業來縮減應用程式大小。他們會在建構期間分析應用程式,並盡可能從未使用的原始碼中移除。
例如,在上述程式碼片段中,最終套件應該只包含 add
函式,因為這是您從 index.js
匯入的 utils.js
中的唯一符號。
讓我們使用以下 webpack
設定建構應用程式:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'production',
};
這裡指定我們使用實際工作環境模式最佳化功能,並使用 index.js
做為進入點。叫用 webpack
後,如果探索「輸出」大小,就會看到如下內容:
$ cd dist && ls -lah
625K Apr 13 13:04 out.js
請注意,套件為 625KB。查看輸出內容後,我們會找出 utils.js
的所有函式以及 lodash
提供的許多模組。雖然我們在輸出內容的 index.js
中沒有使用 lodash
,但這麼做會對實際工作環境的資產造成很多額外權重。
現在,讓我們將模組格式變更為 ECMAScript 模組,然後再試一次。這次 utils.js
看起來會像這樣:
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;
import { maxBy } from 'lodash-es';
export const max = arr => maxBy(arr);
而 index.js
會使用 ECMAScript 模組語法從 utils.js
匯入:
import { add } from './utils.js';
console.log(add(1, 2));
使用相同的 webpack
設定,即可建構應用程式並開啟輸出檔案。現在是 40 個位元組,具有下列 output:
(()=>{"use strict";console.log(1+2)})();
請注意,最終套件不包含我們未使用的 utils.js
函式,lodash
沒有追蹤記錄!更棒的是,terser
(webpack
使用的 JavaScript 壓縮器) 會將 add
函式內嵌在 console.log
中。
您可能會想:為何使用 CommonJS 會使輸出套件的大小變成將近 16,000 倍?當然,這是玩具範例,在現實中,大小差異可能不大,但 CommonJS 會大幅提高您的生產版本權重。
CommonJS 模組通常比 ES 模組的動態更多,因此一般來說要最佳化。為確保您的 Bundler 和壓縮程式能夠成功最佳化應用程式,請避免採用 CommonJS 模組,並在整個應用程式中使用 ECMAScript 模組語法。
請注意,即使您在 index.js
中使用 ECMAScript 模組,如果您使用的模組是 CommonJS 模組,應用程式套件大小也會受到影響。
為什麼 CommonJS 會擴大您的應用程式規模?
為回答這個問題,我們將說明 ModuleConcatenationPlugin
在 webpack
中的行為,然後討論靜態分析。這個外掛程式會將所有模組的範圍串連為單一閉包,讓程式碼在瀏覽器中執行速度更快。讓我們看看以下範例:
// utils.js
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// index.js
import { add } from './utils.js';
const subtract = (a, b) => a - b;
console.log(add(1, 2));
以上是我們匯入 index.js
的 ECMAScript 模組。我們也會定義 subtract
函式。我們可以使用相同的 webpack
設定來建構專案,但這次將會停用最小化原則:
const path = require('path');
module.exports = {
entry: 'index.js',
output: {
filename: 'out.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
minimize: false
},
mode: 'production',
};
讓我們看看產生的輸出內容:
/******/ (() => { // webpackBootstrap
/******/ "use strict";
// CONCATENATED MODULE: ./utils.js**
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;
// CONCATENATED MODULE: ./index.js**
const index_subtract = (a, b) => a - b;**
console.log(add(1, 2));**
/******/ })();
在上方的輸出內容中,所有函式都位於同一個命名空間中。為避免衝突,Webpack 已將 index.js
中的 subtract
函式重新命名為 index_subtract
。
如果壓縮工具處理上述原始碼,就會:
- 移除未使用的函式
subtract
和index_subtract
- 移除所有註解和多餘的空白
- 在
console.log
呼叫中內嵌add
函式的主體
開發人員經常會將這項未使用的匯入作業移除為樹動。唯有在建構期間,Webpack 能夠以靜態方式瞭解從 utils.js
匯入哪些符號,以及所匯出的符號,才能回溯觀察。
系統預設會啟用 ES 模組,因為相較於 CommonJS,這類模組更可靜態分析。
我們來看相同範例,但這次將 utils.js
變更為使用 CommonJS 而非 ES 模組:
// utils.js
const { maxBy } = require('lodash-es');
const fns = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
divide: (a, b) => a / b,
max: arr => maxBy(arr)
};
Object.keys(fns).forEach(fnName => module.exports[fnName] = fns[fnName]);
這項小幅更新會大幅改變輸出內容。嵌入這個頁面的頁面太久了,我只分享其中一小部分:
...
(() => {
"use strict";
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(288);
const subtract = (a, b) => a - b;
console.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__/* .add */ .IH)(1, 2));
})();
請注意,最終套件含有一些 webpack
「執行階段」:插入的程式碼,負責從套裝模組匯入/匯出功能。目前,我們不會將 utils.js
和 index.js
的所有符號放在相同的命名空間下,而是必須在執行階段使用 __webpack_require__
以動態方式處理 add
函式。
這是因為 CommonJS,我們可以從任意運算式取得匯出名稱。舉例來說,以下程式碼是絕對有效的結構:
module.exports[localStorage.getItem(Math.random())] = () => { … };
組合器在建構期間無法得知匯出符號的名稱為何,因為這需要在使用者瀏覽器環境下只能在執行階段取得的資訊。
這樣一來,壓縮器就無法瞭解其依附元件的index.js
實際使用了哪些項目,因而無法將其回溯。我們也會觀察第三方模組的行為是否相同。如果從 node_modules
匯入 CommonJS 模組,建構工具鍊將無法正確進行最佳化。
使用 CommonJS 進行樹木搖晃
CommonJS 模組是由定義動態決定,因此難以分析。舉例來說,ES 模組中的匯入位置一律為字串常值,而 CommonJS 可以是運算式,
在某些情況下,如果您使用的程式庫遵循 CommonJS 的使用特定慣例,您可以使用第三方 webpack
外掛程式,在建構期間移除未使用的匯出項目。雖然這個外掛程式新增對樹狀結構的支援功能,但並未涵蓋依附元件使用 CommonJS 的所有不同方式。也就是說,您取得的保證和 ES 模組的保證也不同。此外,除了預設的 webpack
行為之外,這會導致建構程序中出現額外費用。
結論
為確保 Bundler 能夠成功最佳化應用程式,請避免仰賴 CommonJS 模組,並在整個應用程式中使用 ECMAScript 模組語法。
以下提供幾個可行提示,有助於驗證您走在最佳路徑上:
- 使用 Rollup.js 的 node-resolve
外掛程式,並設定
modulesOnly
旗標,指定只想使用 ECMAScript 模組。 - 使用套件
is-esm
驗證 npm 套件是否使用 ECMAScript 模組。 - 如果使用 Angular,系統會使用無法還原的模組 (非樹狀結構式模組) 顯示警告。