範圍提升

從前,JavaScript 打包器透過將每個模組包覆在一個函式中來運作,而這個函式會在匯入模組時呼叫。這可確保每個模組有獨立的隔離範圍,而且副作用會在預期的時間執行,並啟用開發功能,例如 熱模組替換。然而,所有這些獨立的函式都會有代價,無論是在下載大小或 執行時間效能 方面。

在生產版本中,Parcel 會在可能的情況下將模組串接成單一範圍,而不是將每個模組包覆在獨立的函式中。這稱為「範圍提升」。這有助於使縮小化更有效,並透過讓模組之間的參考變成靜態而不是動態物件查詢,來改善執行時間效能。

Parcel 也會靜態分析每個模組的匯入和匯出,並移除所有未使用的部分。這稱為「樹狀搖晃」「移除無用程式碼」。樹狀搖晃支援靜態和 動態匯入CommonJSES 模組,甚至跨語言支援 CSS 模組

範圍提升如何運作

#

Parcel 的範圍提升實作透過獨立且平行分析每個模組,並在最後將它們串接在一起來運作。為了讓串接成單一範圍是安全的,每個模組的頂層變數會重新命名,以確保它們是唯一的。此外,匯入的變數會重新命名,以符合解析模組的匯出變數名稱。最後,會移除所有未使用的匯出。

index.js
import {add} from './math';

console.log(add(2, 3));
math.js
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 模組 importexport 陳述式、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 模組中的 exportsmodule.exportsthis。這表示屬性名稱必須在建置時靜態得知(即非變數)。

當看到非靜態模式時,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;

避免重新指派 moduleexports

#

當重新指派 CommonJS moduleexports 變數時,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 欄位支援下列值

當檔案標記為沒有副作用時,如果沒有任何已使用的匯出,Parcel 可以跳過整個檔案,同時串接套件。這可以大幅縮小套件大小,特別是如果模組在初始化期間呼叫輔助函式時。

app.js
import {add} from 'math';

console.log(add(2, 3));
node_modules/math/package.json
{
"name": "math"
"sideEffects": false
}
node_modules/math/index.js
export {add} from './add.js';
export {multiply} from './multiply.js';

let loaded = Date.now();
export function elapsed() {
return Date.now() - loaded;
}

在這種情況下,僅使用 math 函式庫中的 add 函式。multiplyelapsed 未使用。通常,仍然需要 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 仍然會被呼叫,以防它有副作用。