メインコンテンツへ移動

webpack から rollup へ

無料2017-09-30#Tool#webpack缺陷#rollup入门指南#slow __webpack_require__#rollup tutorial#babel guide

なぜ webpack を捨てて rollup に切り替えるのか?

一.webpack を放棄する理由

1.webpack モジュールの可読性が低すぎる

// モジュール参照
var _myModule1 = __webpack_require__(0);
var _myModule2 = __webpack_require__(10);
var _myModule3 = __webpack_require__(24);

// モジュール定義
/* 10 */
/***/function (module, exports, __webpack_require__) {...}

// ソースコード
_myModule2.default.xxx()

このようなコードは読むのが非常に大変です。まず _myModule2 に対応する __webpack_require__ id を見つけ、対応するモジュール定義を探し、最後にそのモジュールの exports に何が付属しているかを確認します。このモジュール定義部分が非常に厄介で、参照チェーンの読み取りを長くします

もちろん、通常は bundle を読む必要はないので、これは致命的ではありません

2.ファイルが非常に大きい

上記で述べたように、これらの追加の bundle コード(サブモジュール定義、サブモジュール参照など)によりファイルサイズが膨張します。理由は以下の通り:

  • ソースコードの各独立ファイルの外側にモジュール定義のレイヤーが包まれている

  • モジュール内の他のモジュールへの参照に __webpack_require__ 宣言が挿入されている

  • __webpack_require__ ツール関数自体のサイズ

ファイルサイズは転送負荷をもたらすだけでなく、Compile 時間にも影響します。パッケージングソリューションの bundle size は重要な指標です

3.実行が非常に遅い

サブモジュール定義とランタイム依存関係処理__webpack_require__)は、ファイルサイズの増大だけでなく、パフォーマンスも大幅に低下させます。下図の通り:

(画像は webpack_require is too slow より)

パッケージングソリューションはパフォーマンスに大きな影響を与える ことは、最も致命的で、耐えられない点です

二.rollup の利点

1.ファイルが非常に小さい

ほぼ余分なコードがなく、必要な cjs, umd ヘッダーを除けば、bundle コードは基本的にソースコードと変わらず、奇妙な __webpack_require__, Object.defineProperty などはありません

bundle サイズの比較は以下の通り:

webpack 132KB
rollup  82KB

2.実行が非常に速い

上記で述べたように、余分なコードがほとんどないため、webpack bundle はサイズが大きいだけでなく、非ビジネスコード(__webpack_require__, Object.defineProperty)の実行時間も無視できません

rollup はこれらの追加のものを生成しないため、実行時間は主に Compile ScriptEvaluate Script にあり、残りの部分は無視できます。下図の通り:

[caption id="attachment_1531" align="alignnone" width="844"]rollup-performance rollup-performance[/caption]

3.es モジュールおよび iife 形式のサポート

// rollup
amd – Asynchronous Module Definition, used with module loaders like RequireJS
cjs – CommonJS, suitable for Node and Browserify/Webpack
es – Keep the bundle as an ES module file
iife – A self-executing function, suitable for inclusion as a <script> tag. (If you want to create a bundle for your application, you probably want to use this, because it leads to smaller file sizes.)
umd – Universal Module Definition, works as amd, cjs and iife all in one

// webpack
"var" - Export by setting a variable: var Library = xxx (default)
"this" - Export by setting a property of this: this["Library"] = xxx
"commonjs" - Export by setting a property of exports: exports["Library"] = xxx
"commonjs2" - Export by setting module.exports: module.exports = xxx
"amd" - Export to AMD (optionally named - set the name via the library option)
"umd" - Export to AMD, CommonJS2 or as property in root

es6 モジュールのパッケージングをサポートしており、ベースライブラリなどに適しています。es6 プロジェクトは通常 babel で一度変換するため、1 回 の統一された babel 変換を保証できます

iife へのパッケージングをサポートしており、非常に小さくなります。また、最終 bundle サイズから見ると:

        default uglify
cjs     81KB    34K
amd     81KB    30KB
iife    81KB    30KB
umd     82KB    30KB

umdcjs より有利 で、奇妙に見えますが、実際の結果は確かにそうです。bundle の違いは主に機能名の単純化にあり、cjs bundle には多くの長い機能名が保持されており、難読化されていません

三.rollup の欠点

最新バージョン(0.50.0)はまだ 0.x の不安定な状態にあり、バージョン関連の問題が多いです(特定の問題はバージョンダウングレードで解決する必要がある場合さえあります)

例えば、複数の依存ライブラリをパッケージングし、共通の依存項を抽出する(webpack の CommonsChunkPlugin)

  • 以前のバージョン(0.43)は循環依存関係の処理が不十分 で、パッケージング/実行エラーが発生する可能性があります

  • ドキュメントが比較的多くなく、問題に遭遇しても迅速に解決できません

例えば、一般的なエラー 'foo' is not exported by bar.js (imported by baz.js)Troubleshooting は FAQ ですが、詳細で信頼できる解決策を提供していません(つまり、従っても解決できない可能性があります)

四.babel 設定

babel 変換は一般的に不可欠で、rollup/webpack パッケージングプロセスの中間処理环节として、相应的なラッパープラグインを提供しており、babel 設定を埋め込むことができます。実際に掌握する必要があるのは babel 設定です

babel preset

In Babel, a preset is a set of plugins used to support particular language features.

一般的なものは以下の通り:

  • es2015:ES6 特性のみをサポート。preset にこの項目がある場合、ES6 構文を ES5 に変換します

  • stage-0:最新の es7 や es8 特性もサポート。実際には ES Stage 0 Proposals を指します。preset にこの項目がある場合、ESn を ES6 に変換します

  • react:React JSX をサポート

stage-0 は最も過激なアプローチで、babel が変換できる すべての JS 新機能を使用することを意味し、安定しているかどうかは関係ありません。es2015 は最も保守的で、仕様はすでにリリースされており、機能が不安定なリスクはありません。stage-0 のように打てるものがさらに 4 つあります(TC39 仕様策定プロセス):

  • stage-0 - Strawman: just an idea, possible Babel plugin.
  • stage-1 - Proposal: this is worth working on.
  • stage-2 - Draft: initial spec.
  • stage-3 - Candidate: complete spec and initial browser implementations.
  • stage-4 - Finished: will be added to the next yearly release.

P.S.最近 babel は babel-preset-env を提供しており、ターゲットプラットフォーム環境に応じて自動的に preset を追加します。そのため、一堆の esxxx をインストールする必要はありませんが、ES サポートのみを提供し、react や polyfill は内置されていませんし、内置されるべきでもありません。env に関する詳細情報は babel-preset-env: a preset that configures Babel for you をご覧ください

注意、各 preset は 1 段階の変換のみを担当します。例えば stage-0 は ESn を ES6 に変換できますが、ES5 ではありません。つまり、構文が非常に過激なプロジェクトを ES5 に変換したい場合、以下のような babel 設定が必要です:

{
  "presets": [
    ["stage-0"],
    ["es2015",  {"modules": false}]
  ],
  "plugins": [
    "external-helpers"
  ]
}

P.S.その中で、{"modules": false}rollup が必要で、babel-preset-es2015-rollup の代わりに使用します。external-helpers の役割は後で紹介します

ES6 スタイルを保持したい場合、以下のような babel 設定が必要です:

{
  "presets": [
    ["stage-0"]
  ],
  "plugins": [
    "external-helpers"
  ]
}

変換後に得られるのは、プロジェクトの各モジュールファイルを一緒に繋げたES6 モジュール ��、コード内の classconstlet はすべて保持されます。ES6 はこれらの特性をサポートしているためですが、async&await などのより高度な特性は ES6 に変換されます

babel plugin

babel の 3 つの処理环节において:

parsing -> transforming -> generation

プラグインは 2 番目の环节(transforming)に作用します。つまり、ソース構文の解析完了後、それを等価なターゲット構文に変換する段階で、プラグインを通じてさらに処理できます。例えば簡単なもの:

// 識別子メンバーアクセスをリテラル形式に変換、例えば a.catch -> a['catch']
es3-member-expression-literals
// 識別子メンバー宣言をリテラル形式に変換、例えば{catch: xxx} -> {'catch': xxx}
es3-property-literals

また一般的なものは以下の通り:

// class 静的プロパティとインスタンスプロパティをサポート、例えば class A{instanceProp = 1; static staticProp = 2;}
transform-class-properties
// babel 自身が使用する共通メソッドを抽出、例えば_createClass, _inherits など
external-helpers
// 定数変更チェック、const 宣言の定数が変更された場合にエラーを報告
check-es2015-constants

したがって babel plugin は大まかに3 種類に分類されます:

  • ES5/ES6 パッチ、より低い環境に関連する問題を修正(es3-xxx、es2015-xxx)

  • 静的チェック、例えば const 変更エラーを「コンパイル」段階に前倒し

  • リスク特性、例えば class-properties など stage に含めるのに適さない争议的な特性

パッチは生産環境を対象とし、静的チェックは品質保証の一部であり、リスク特性はより過激な JS 構文です

babel polyfill

babel が ESn 高度構文を ES5/ES3 に変換する際、4 つの状況に遭遇します:

  • 単純な構文糖。無脳変換、例えば for...of, arrow function

  • 複雑な構文糖。ツール関数処理が必要、例えば createClass, inherits

  • 低い環境に不足している基礎特性。polyfill が必要、例えば Symbol, Promise, String.repeat

  • polyfill できない特性。例えば Proxy

低い環境に不足している基礎特性について、babel はデフォルトで polyfill を提供しません(babel 変換結果には polyfill が含まれていません)。babel-polyfill を導入するか、必要な特殊な polyfill(より軽量で小型のもの、またはより信頼性の高い重量級のものを)導入できます

babelHelpers

babel には変換関連のツール関数があります。例えば:

_typeof
_instanceof
_createClass
_interopRequireDefault
_classCallCheck
_inherits
asyncGenerator

これらのツール関数はすべて babelHelpers に属し、完全な helpers はコマンドで生成できます:

npm install babel-cli --save-dev
// type 選択可能 global/umd/var
./node_modules/.bin/babel-external-helpers -t umd > helpers.js

P.S.babelHelpers 生成に関する詳細情報は External helpers をご覧ください

デフォルト設定では、これらのツール関数は複数回生成され、つまり bundle 内に複数の _createClass 宣言が存在し、冗長コードです。プラグイン設定で最適化または削除できます

デフォルト設定、bundle 内に複数の helper 宣言が存在:

{
  "presets": [
    ["es2015"]
  ]
}

external-helpers プラグインを追加し、helper 宣言を bundle 顶部に抽出し、複数の宣言が存在しないように:

{
  "presets": [
    ["es2015"]
  ],
  "plugins": [
    "external-helpers"
  ]
}

外部 babelHelpers を参照し、bundle 内に helper 宣言を含まない:

{
  "presets": [
    ["es2015"]
  ],
  "plugins": [
    "external-helpers"
  ],
  externalHelpers: true
}

一般的に external-helpers を追加して helper を bundle 顶部に抽出すれば最適化要件を満たせるため、babel 設定は少なくとも external-helpers プラグインを追加して冗長 helper コードを削除することを推奨します

externalHelpers: true複数の bundle(multi entry)の状況针对で、追加しないと各 bundle 顶部に 1 つの helper 宣言があり、追加後は bundle すべてが外部 helper を参照します。例えば:

babelHelpers.createClass(xxx)

babelHelpers は bundle 内で未定義のため、事前に導入する必要があります。例えば web 環境:

<script src="babelHelpers.js"></script>
<script src="bundle.js"></script>

五.まとめ

webpack と比較して、rollup は比類のないパフォーマンス利点を持っています。これは依存関係処理方法によって決定され、コンパイル時依存関係処理(rollup)は当然ランタイム依存関係処理(webpack)よりパフォーマンスが良くなりますが、循環依存関係の処理は 100% 信頼できません。内部実装(または設計)を通じて回避するように努め、循環依存関係を解決する一般的なテクニック は以下の通り:

  • 依存関係の引き上げ、相互に依存する部分を 1 レベル引き上げる

  • 依存関係の注入、ランタイムにモジュール外部から依存関係を注入

  • 依存関係の検索、ランタイムにモジュール内部で依存関係を検索

依存関係の引き上げは不合理な設計针对で、此类の循環依存関係は本来回避可能です。例えば A->B, B->AC を提案して A->C, B->C に変換できる可能性があります

回避できない循環依存関係については、ランタイム依存関係注入と依存関係検索で解決できます。例えば factory->A, A->factory、簡単な依存関係注入方案は:

// factory.js
import A from './A';
export create() {
    // 構築関数注入
    return new A(create);
    // プロパティ注入
    // let a = new A();
    // a._createFromFactory = create;
    // return a;
}

// A.js
class A {
    constructor(create) {
        this._createFromFactory = create;
    }
    // factory から注入される
    _createFromFactory() {
        return null;
    }
}

したがって循環依存関係は設計/実装から解決可能で、大きな問題ではありません

应用场景而言、rollup は単一ファイルへのパッケージングに最も適しています。現在 rollup は multi entry にあまり友好的ではないためです(共通依存項を抽出できません)。また、安定性およびプラグインエコシステム、ドキュメントなどは webpack に劣りますが、パフォーマンスを厳しく要求 する场景では、rollup が唯一の選択肢です

参考資料

コメント

コメントはまだありません

コメントを書く