範圍提升
從前,JavaScript 打包器透過將每個模組包覆在一個函式中來運作,而這個函式會在匯入模組時呼叫。這可確保每個模組有獨立的隔離範圍,而且副作用會在預期的時間執行,並啟用開發功能,例如 熱模組替換。然而,所有這些獨立的函式都會有代價,無論是在下載大小或 執行時間效能 方面。
在生產版本中,Parcel 會在可能的情況下將模組串接成單一範圍,而不是將每個模組包覆在獨立的函式中。這稱為「範圍提升」。這有助於使縮小化更有效,並透過讓模組之間的參考變成靜態而不是動態物件查詢,來改善執行時間效能。
Parcel 也會靜態分析每個模組的匯入和匯出,並移除所有未使用的部分。這稱為「樹狀搖晃」或「移除無用程式碼」。樹狀搖晃支援靜態和 動態匯入、CommonJS 和 ES 模組,甚至跨語言支援 CSS 模組。
範圍提升如何運作
#Parcel 的範圍提升實作透過獨立且平行分析每個模組,並在最後將它們串接在一起來運作。為了讓串接成單一範圍是安全的,每個模組的頂層變數會重新命名,以確保它們是唯一的。此外,匯入的變數會重新命名,以符合解析模組的匯出變數名稱。最後,會移除所有未使用的匯出。
import {add} from './math';
console.log(add(2, 3));
export function add(a, b) {
return a + b;
}
export function square(a) {
return a * a;
}
編譯成類似以下內容
function $fa6943ce8a6b29$add(a, b) {
return a + b;
}
console.log($fa6943ce8a6b29$add(2, 3));
正如您所見,add
函數已重新命名,且參考已更新以符合。square
函數已移除,因為它未被使用。
這會產生比每個模組都包在函數中還要小且快的輸出。不僅沒有額外的函數,也沒有 exports
物件,且對 add
函數的參考是靜態的,而不是屬性查詢。
避免中止
#Parcel 可以靜態分析許多模式,包括 ES 模組 import
和 export
陳述式、CommonJS require()
和 exports
指派、動態 import()
解構和屬性存取,以及更多。然而,當遇到無法在事前靜態分析的程式碼時,Parcel 可能必須「中止」並將模組包在函數中,以保留副作用或允許在執行階段解析匯出。
若要判斷為何 tree shaking 未如預期發生,請使用 --log-level verbose
CLI 選項執行 Parcel。這會為發生的每個中止列印診斷,包括顯示導致中止的程式碼框架。
parcel build src/app.html --log-level verbose
動態成員存取
#Parcel 可以靜態解析在建置時已知的成員存取,但當使用動態屬性存取時,模組的所有匯出都必須包含在建置中,且 Parcel 必須建立一個匯出物件,以便可以在執行階段解析值。
import * as math from './math';
// ✅ Static property access
console.log(math.add(2, 3));
// 🚫 Dynamic property access
console.log(math[op](2, 3));
此外,Parcel 沒有追蹤將命名空間物件重新指派給另一個變數。任何使用匯入命名空間,而不是靜態屬性存取,都會導致包含所有匯出。
import * as math from './math';
// 🚫 Reassignment of import namespace
let utils = math;
console.log(utils.add(2, 3));
// 🚫 Unknown usage of import namespace
doSomething(math);
動態匯入
#Parcel 支援使用靜態屬性存取或解構的 tree shaking 動態匯入。這同時支援 await
和 Promise then
語法。然而,如果以任何其他方式存取從 import()
傳回的 Promise,Parcel 必須保留已解析模組的所有匯出。
注意:對於 await
案例,僅當 await
未轉譯(即使用現代瀏覽器清單設定檔)時,才能移除未使用的匯出。
// ✅ Destructuring await
let {add} = await import('./math');
// ✅ Static member access of await
let math = await import('./math');
console.log(math.add(2, 3));
// ✅ Destructuring Promise#then
import('./math').then(({add}) => console.log(add(2, 3)));
// ✅ Static member access of Promise#then
import('./math').then(math => console.log(math.add(2, 3)));
// 🚫 Dynamic property access of await
let math = await import('./math');
console.log(math[op](2, 3));
// 🚫 Dynamic property access of Promise#then
import('./math').then(math => console.log(math[op](2, 3)));
// 🚫 Unknown use of returned Promise
doSomething(import('./math'));
// 🚫 Unknown argument passed to Promise#then
import('./math').then(doSomething);
CommonJS
#除了 ES 模組,Parcel 也可以分析許多 CommonJS 模組。Parcel 支援靜態指定到 CommonJS 模組中的 exports
、module.exports
和 this
。這表示屬性名稱必須在建置時靜態得知(即非變數)。
當看到非靜態模式時,Parcel 會建立一個 exports
物件,所有匯入模組在執行時都會存取該物件。所有匯出都必須包含在最終建置中,且無法執行樹狀搖晃。
// ✅ Static exports assignments
exports.foo = 2;
module.exports.foo = 2;
this.foo = 2;
// ✅ module.exports assignment
module.exports = 2;
// 🚫 Dynamic exports assignments
exports[someVar] = 2;
module.exports[someVar] = 2;
this[someVar] = 2;
// 🚫 Exports re-assignment
let e = exports;
e.foo = 2;
// 🚫 Module re-assignment
let m = module;
m.exports.foo = 2;
// 🚫 Unknown exports usage
doSomething(exports);
doSomething(this);
// 🚫 Unknown module usage
doSomething(module);
在匯入方面,Parcel 支援靜態屬性存取和 require
呼叫的解構。當看到非靜態存取時,必須包含已解析模組的所有匯出,且無法執行樹狀搖晃。
// ✅ Static property access
const math = require('./math');
console.log(math.add(2, 3));
// ✅ Static destructuring
const {add} = require('./math');
// ✅ Static property assignment
const add = require('./math').add;
// 🚫 Non-static property access
const math = require('./math');
console.log(math[op](2, 3));
// 🚫 Inline require
doSomething(require('./math'));
console.log(require('./math').add(2, 3));
避免 eval
#eval
函式會在目前的範圍內執行字串中的任意 JavaScript 程式碼。這表示 Parcel 無法重新命名範圍內的任何變數,以防 eval
存取這些變數。在此情況下,Parcel 必須將模組包覆在一個函式中,並避免縮小變數名稱。
let x = 2;
// 🚫 Eval causes wrapping and disables minification
eval('x = 4');
如果您需要從字串執行 JavaScript 程式碼,您可能可以使用 Function 建構函式。
避免頂層 return
#CommonJS 允許在模組的最上層(即函式外)使用 return
陳述式。當看到此情況時,Parcel 必須將模組包覆在一個函式中,以便僅停止執行該模組,而不是整個套件。此外,由於無法靜態得知輸出(例如,如果回傳值有條件),因此會停用樹狀搖晃。
exports.foo = 2;
if (someCondition) {
// 🚫 Top-level return causes wrapping and disables tree shaking
return;
}
exports.bar = 3;
避免重新指派 module
和 exports
#當重新指派 CommonJS module
或 exports
變數時,Parcel 無法靜態分析模組的輸出。在這種情況下,必須將模組包覆在一個函式中,並停用樹狀搖晃。
exports.foo = 2;
// 🚫 Exports reassignment causes wrapping and disables tree shaking
exports = {};
exports.foo = 5;
避免條件式 require()
#與僅允許在模組最上層使用的 ES 模組 import
陳述式不同,require
是一個函式,可以從任何地方呼叫。但是,當從條件式或其他控制流程陳述式中呼叫 require
時,Parcel 必須將已解析的模組包覆在一個函式中,以便在正確的時間執行副作用。這也遞迴套用於已解析模組的任何依賴項。
// 🚫 Conditional requires cause recursive wrapping
if (someCondition) {
require('./something');
}
副作用
#許多模組僅包含宣告,例如函式或類別,但有些模組也可能包含副作用。例如,模組可能會在 DOM 中插入某些內容、將某些內容記錄到主控台、指派給全域變數(即多重載入)或初始化單例。即使模組的輸出未使用,也必須始終保留這些副作用才能讓程式正確運作。
預設情況下,Parcel 會包含所有模組,這可確保始終執行副作用。但是,package.json
中的 sideEffects
欄位可用於提供提示給 Parcel 和其他工具,說明您的檔案是否包含副作用。對於包含在套件的 package.json 檔案中的函式庫來說,這是最有意義的。
sideEffects
欄位支援下列值
false
– 此套件中的所有檔案都沒有副作用。字串
– 包含副作用檔案的 glob 比對。陣列<字串>
– 包含副作用檔案的 glob 比對陣列。
當檔案標記為沒有副作用時,如果沒有任何已使用的匯出,Parcel 可以跳過整個檔案,同時串接套件。這可以大幅縮小套件大小,特別是如果模組在初始化期間呼叫輔助函式時。
import {add} from 'math';
console.log(add(2, 3));
{
"name": "math"
"sideEffects": false
}
export {add} from './add.js';
export {multiply} from './multiply.js';
let loaded = Date.now();
export function elapsed() {
return Date.now() - loaded;
}
在這種情況下,僅使用 math
函式庫中的 add
函式。multiply
和 elapsed
未使用。通常,仍然需要 loaded
變數,因為它包含在模組初始化期間執行的副作用。但是,由於 package.json
包含 sideEffects
欄位,因此可以完全略過 index.js
模組。
除了大小優點外,使用 sideEffects
欄位也有建置效能優點。在上述範例中,由於 Parcel 知道 multiply.js
沒有副作用,而且沒有使用任何匯出,因此它甚至從未編譯過。但是,如果改用 export *
,則不適用,因為 Parcel 不知道有哪些匯出可用。
sideEffects
的另一個好處是它也適用於套件。如果模組匯入 CSS 檔案或包含動態 import()
,如果模組未用,則不會建立套件。
PURE 註解
#您也可以使用 /*#__PURE__*/
註解為個別函式呼叫加上註解,這會告訴縮小程式,當結果未用時,可以安全地移除該函式呼叫。
export const radius = 23;
export const circumference = /*#__PURE__*/ calculateCircumference(radius);
在這個範例中,如果未用 circumference
匯出,則也不會包含 calculateCircumference
函式。沒有 PURE 註解,calculateCircumference
仍然會被呼叫,以防它有副作用。