スコープホイスト
従来、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は副作用を維持したり、エクスポートを実行時に解決できるようにしたりするために、「ベイルアウト」してモジュールを関数でラップする必要がある場合があります。
ツリーシェイキングが期待どおりに発生しない理由を特定するには、`--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は、静的プロパティアクセスまたは分割代入による動的インポートのツリーシェイキングをサポートしています。 これは、`await`とPromiseの`then`構文の両方でサポートされています。 ただし、`import()`から返されたPromiseが他の方法でアクセスされた場合、Parcelは解決されたモジュールのすべてのエクスポートを保持する必要があります。
**注:** `await`の場合、残念ながら、`await`がトランスパイルされない場合(つまり、最新のbrowserslist設定を使用する場合)にのみ、使用されていないエクスポートを削除できます。
// ✅ 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コードを実行します。 これは、`eval`によってアクセスされる場合に備えて、Parcelがスコープ内の変数の名前を変更できないことを意味します。 この場合、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` – このパッケージのすべてのファイルに副作用はありません。
- `string` – 副作用を含むファイルに一致するglob。
- `Array<string>` – 副作用を含むファイルに一致する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`のもう1つの利点は、バンドリングにも適用されることです。 モジュールがCSSファイルをインポートするか、動的な`import()`を含んでいる場合、モジュールが使用されていないとバンドルは作成されません。
PUREアノテーション
#個々の関数呼び出しに /*#__PURE__*/
コメントを付与することもできます。これは、結果が使用されない場合、ミニファイアーがその関数呼び出しを安全に削除できることを示します。
export const radius = 23;
export const circumference = /*#__PURE__*/ calculateCircumference(radius);
この例では、circumference
エクスポートが使用されていない場合、calculateCircumference
関数も含まれません。PURE アノテーションがない場合、calculateCircumference
は副作用がある場合に備えて呼び出されます。