babel-preset-babili を試す
babel-preset-env と minify の続き。前回は ES.next なコードを minify する方法として uglify-js を中心に babel-preset-babili を少しだけ試したところで終わった。今回は後者の使い方を掘り下げる。
Babel における plugin と preset
babel-preset-babili は ES.next な JavaScript を ES5 以降の書式に変換する Babel 関連のツールで minify を担当。
現在の Babel 本体はランタイムに徹し、実際のコード解析や変換はランタイム上で動作する plugin により処理される。開発者は機能ごとに plugin を組み合わせることになるが、これらは膨大である。そのため直に利用せず plugin 集となる preset を選ぶほうがよいだろう。
例えば以下のような preset がある。
preset | 用途 |
---|---|
babel-preset-env | 対象とする動作環境 (Web ブラウザーなど) の組み合わせやバージョンを指定することで ECMAScript 6 compatibility table に基き ES.next を ES5 以降の形式へ必要最小に変換する。標準では babel-preset-latest 相当の全変換を実施。latest は deprecated になったので全変換の場合でも env を使用することが望ましい。 |
babel-preset-babili | ES.next を minify する。標準では minify のみ。細かな設定は preset を構成する plugin 単位で指定。 |
babel-preset-react | React の JSX や Flow などを変換する。Flow は babel-preset-flow という単体版もあるが、この preset はそれを組み込んでいる。このように preset は他の preset も含められる。 |
Babel の設定は .babelrc
ファイルや package.json
の babel
プロパティに定義。env と babili の場合、標準設定であれば
{
"presets": ["babili"]
}
のように presets
Array へ preset 名だけを String として記述。preset に含まれる pluguin を個別に設定するなら
{
"presets": [["babili", {
"mangle": {
"blacklist": {
"ParserError": true,
"NetworkError": false
}
},
"unsafe": {
"typeConstructors": false
},
"keepFnName": true
}]]
}
のように preset を String
から Array
へ書き換えて、第二要素の Object
へ plugin 単位のプロパティを記述してゆく。
ここからは babili を構成する plugin とその設定についてまとめる。オプションについては README に型や既定値が掲載されていないため GitHub に公開されているコードも参考にした。
babel-plugin-minify-constant-folding
リテラル同士の演算を定数に変換する plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"evaluate": true
}]]
}
例えば "a" + "b"
は "ab"
となり、"b" + a + "c" + "d"
のように変数が含まれるなら "b" + a + "cd"
という感じでそこをを避けてくれる。
babel-plugin-minify-dead-code-elimination
dead code (到達不能コード) を除去する plugin。babili 標準で有効、オプションは以下。
設定 | 型 | 既定値 | 内容 |
---|---|---|---|
optimizeRawSize |
Boolean | false |
README のサンプルに記載されているものの、設定値については解説されておらず用途不明。実装をみるとスコープと変数 bind に関する最適化の実行フラグになっている。 |
keepFnName |
Boolean | false |
元の関数名を維持する。 |
keepFnArgs |
Boolean | false |
関数の引数を維持する。 |
keepClassName |
Boolean | false |
元のクラス名を維持する。 |
{
"presets": [["babili", {
"deadcode": {
"optimizeRawSize": false,
"keepFnName": false,
"keepFnArgs": false,
"keepClassName": false
}
}]]
}
既定値のまま変換すると function foo() {var x = 1;}
は function foo() {}
になる。関数のようにインターフェースとして露出している部分は維持しつつ、ローカルの範疇で解析可能な dead code を除去するようだ。
babel-plugin-minify-infinity
JavaScript のグローバルな Object である Infinity を 1 / 0;
に変換する plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"infinity": true
}]]
}
他のプラットフォームではゼロ除算を致命的なエラーとすることが多い。しかし JavaScript だと結果は Infinity
になる。
という仕様を踏まえて Object
から演算に変換していると思われるが、なぜこの処理が必要なのかは不明。Object
を演算にすることで Infinity
への代入を構文エラーとして検出したいとか、そんな目的があるのだろうか?
babel-plugin-minify-mangle-names
コンテキストやスコープを考慮して Name mangling (名前修飾) 処理を実行得する plugin。babili 標準で有効、オプションは以下。
設定 | 型 | 既定値 | 内容 |
---|---|---|---|
blacklist |
Object | {} |
変換対象から除外する識別子の設定。例えば foo という名前を除外したい場合は {"foo":true} のように指定する。 |
eval |
Boolean | false |
eval がアクセス可能な範囲で mangle を有効にする。 |
keepFnName |
Boolean | false |
元の関数名を維持する。 |
topLevel |
Boolean | false |
最上位スコープに対する mangle を有効にする。 |
keepClassName |
Boolean | false |
元のクラス名を維持する。 |
{
"presets": [["babili", {
"mangle": {
"blacklist": {
"foo": true
},
"eval": false,
"keepFnName": false,
"topLevel": false,
"keepClassName": false
}
}]]
}
mangle 処理により変数、関数、クラス名などが a
や aa
といった短い名前に変更される。結果、ファイル サイズの削減や単純な難読化といった効果を得られる。
babel-plugin-minify-numeric-literals
数値リテラルを可能な範囲で短縮する plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"numericLiterals": true
}]]
}
例えば 1000
を 1e3
のように指数表記へ置き換える。実装をみると Number.prototype.toExponential() の結果から /\+/g
と /e0/
を空文字に置換したものとなり、元の表記よりも長い場合はキャンセルされる。
babel-plugin-minify-replace
ユーザーが明示した設定に従って置換を実行する plugin。babili 標準で有効、オプションは以下。
設定 | 型 | 既定値 | 内容 |
---|---|---|---|
replacements |
Array | 置換対象コレクション。 | |
[].identifierName |
String | 置換対象とする識別子の名前。 | |
[].replacement |
Object | 置換方法。 | |
[].replacement.type |
String | 置換対象となる識別子の型。サンプルを見るに Babel types の *Literal 系を指定するようだ。 |
|
[].replacement.value |
Any | 置換する値。型は replacement.type に指定されたものと対応。 |
{
"presets": [["babili", {
"replace": {
"replacements": [
{
"identifierName": "DEBUG",
"replacement": {
"type": "numericLiteral",
"value": 0
}
}
]
}
}]]
}
識別子を直に置換することからマクロのようなメタ プログラミング、要するに他のプラットフォームでいうところの pre-processing を実現可能。uglify-js の --define
オプションによる ifdef DEBUG
処理の代替になる。
babel-plugin-minify-simplify
ステートメントを式に変換してコードを短くする plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"simplify": true
}]]
}
例えば if (x) a();
は短絡評価を利用して x && a();
になる。if (x) { a(); } else { b(); }
は三項演算に展開され x ? a() : b();
となる。ただし圧縮率を高めるため undefined
を void 0
、foo['bar']
を foo.bar
に変換する処理はハック的なコードに対して副作用を及ぼすかもしれない。
babel-plugin-transform-merge-sibling-variables
ステートメントの別れた変数の宣言をひとつにまとめる plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"mergeVars": true
}]]
}
例えば var a = 0; var b = 2;
を var a = 0, b = 0;
に変換。for 文の外で宣言され for 文内でしか使用されていない変数は for の宣言に移動される。
babel-plugin-transform-minify-booleans
Boolean リテラルをより短い表記に変換する plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"booleans": true
}]]
}
true
を !0
、false
は !1
になる。短縮される文字数こそ少ないが、リテラルのみを対象とするため副作用の心配もない。
babel-plugin-transform-regexp-constructors
RegExp コンストラクターを正規表現リテラルに変換する plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"regexpConstructors": true
}]]
}
例えば
const foo = 'ab+';
var a = new RegExp(foo+'c', 'i');
は
const foo = 'ab+';
var a = /ab+c/i;
になる。演算子 new
も含めると RegExp インスタンス生成の表記はかなり長いため、これを使用しているなら相当の短縮が見込める。
babel-plugin-transform-remove-console
console.*
の呼び出しを削除する plugin。babili 標準では無効、オプションなし。
{
"presets": [["babili", {
"removeConsole": false
}]]
}
ライブラリーやフレームワークの場合、ユーザーが開発者なので残したほうがよい。アプリ層については plugin を有効にして余計な出力を除去、といった使い分けをする。
babel-plugin-transform-remove-debugger
debugger ステートメントを削除する plugin。babili 標準では無効、オプションなし。
{
"presets": [["babili", {
"removeDebugger": false
}]]
}
私はこれまで使用したことはなかったのだが、この debugger ステートメントは Web ブラウザーの開発者ツールにおいてコード上に記述するブレーク ポイントとして機能するそうだ。コード上なので到達すると常にブレークする。JavaScriptのデバッグ方法 – JSを嫌いにならないためのTips によるとコールバック関数まわりのデバッグに役立つらしい。
同記事で「当然、本番用のコードに残しておきたいものではありません。」とあるとおり開発用の機能なので、アプリ層ならば babili で除去したほうがよい。
babel-plugin-transform-remove-undefined
変数への代入や戻り値の指定された undefined
を削除する plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"removeUndefined": true
}]]
}
例えば var b = undefined;
は var b;
、return undefined;
が return;
になる。変数なら対象は関数内に限定されるため副作用の心配はない。
babel-plugin-transform-undefined-to-void
undefined
参照を void 0
に変換する plugin。babili 標準で有効、オプションなし。
{
"presets": [["babili", {
"undefinedToVoid": true
}]]
}
前述の simplify でも同様の処理がおこなわれるのでは?と思ったが、あちらはステートメントを対象としている。こちらは foo === undefined;
を foo === void 0
のように参照を変換。plugin の説明を読みと simplify で予想したとおり undefined
の上書き事故を防ぐことが目的とのこと。
Electron 向け minify を uglify-js から babili に変更する
本記事の冒頭に紹介した前回記事では
- babili の細かなカスタマイズには plugin 学習が必要なこと
package.json
におけるdependencies
の npm が minify されないこと
を課題として残した。これらのうち前者は今回の記事で対応。基本、babili 標準で問題ないだろう。後者については babili というより browserify + babelify という組み合わせの問題。状況を把握しやすくするため処理遷移をまとめる。
- Browserify 実行
- Browserify が transpile のために babelify を実行
- babelify が presets の順番に従い JavaScript を transpile
- presets の babel-preset-env により ES.next が ES5 + α な JavaScript へ transpile
- presets の babel-preset-babili により ES5 + α な JavaScript を minify
- Browserify が transpile された JavaScript 内の require を解決して単一ファイルに bundle (結合)
uglify-js と異なり babili は babelify から実行されるため、この時点では dependencies
の npm は対象外。よってアプリ側のファイルだけ transpile + minify され、その後に dependencies
側がそのまま bundle されてしまう。React のように巨大な npm だとファイル サイズ的に問題となるし NODE_ENV=production
を経ていないためデバッグ用のコードも残る。
そのため
- Browserify 以降に minify を実行する
- babelify 時点でなんとか
dependencies
も対象とする
として 1 はビルド プロセスが複雑になる。これをおこなうぐらいなら babel-preset-env を全変換で実行して uglify-js するほうがよい。または前回記事で触れた uglify-js harmony 版を使用するとか。2 については babili の対象を node_modules
まで広げる必要がある。しかし Babel は bundler にあらず、参照まで辿って処理はしない。
詰んだかも?しかし React に限定すれば npm に minify 版も付属するため babili を利用して参照を書き換えてしまえばよさそう。というわけで
{
"presets": [["babili", {
"replace": {
"replacements": [
{
"identifierName": "react",
"replacement": {
"type": "stringLiteral",
"value": "react/dist/react.min"
}
},
{
"identifierName": "DEBUG",
"replacement": {
"type": "numericLiteral",
"value": 0
}
}
]
}
}]]
}
この設定により react
を react/dist/react.min
に書き換えようとしたのだが失敗。identifierName
と value
の内容を '
で囲って type
に identifier
を指定してもダメだった。replace のテストは文字列リテラルの書き換えがないため、そもそもサポートしていないのだろう。
Browserify の exclude/require
では env や babili とは別に import 対象を変更する plugin はないか?と探してみたら babel-plugin-transform-rename-import を見つけた。
"plugins": [
[
"transform-rename-import",
{
"original": "react",
"replacement": "react/dist/react.min"
}
]
]
そして上記のように設定して実行してみたが、置換元と同じ名前が置換先に含まれていると重複変換されるようで
Error: Cannot find module 'react/dist/react/dist/react' from '.../examples-electron/audio-player/src/js/renderer/main'
というエラーになってしまった。仕方ないので Browserify として exclude + require することに。以下はそのコマンド。
cross-env NODE_ENV=production browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron --exclude react --exclude react-dom --require ./node_modules/react/dist/react.min.js:react --require ./node_modules/react-dom/dist/react-dom.min.js:react-dom -o ./dist/src/assets/renderer.js
めちゃくちゃ長くなってしまった。この処理で生成した renderer.js
について
class
など ES2015 以降の予約語が維持されること- Electron アプリとして動作すること
を確認したのだが、当然ながら Browserify の bundle 用コードは minify されない。そのためファイル サイズが肥大化した。
env | minify | size |
---|---|---|
標準 (latest 相当) | uglify-js | 約 320KB |
Electron | babili | 約 400KB |
dependencies
をまったく使わないか React のように重量級の npm を使用しないなら babili でもよさそう。Electron は Node として require できるから bundler なしにする手もある。ただし私が bundler を使用するにはアプリの配布イメージから node_modules
を除外する目的もあるため、この案は却下せざるをえない。
babel-cli + babili
そもそも transpile と bundle が分離しているのだから、uglify-js のようにビルド処理の最後に minify を実装するのはどうか?ということで処理を組み替えてみた。package.json
から必要最小の抜粋。babili の設定は長くなるのでとりあえず標準。
{
"babel": {
"presets": [
["env", {"targets": {"electron": 1.6}}],
"react"
],
"env": {
"production": {
"presets": ["babili"]
}
}
},
"scripts": {
"bundle": "browserify -t [ babelify ] ./src/js/renderer/App.js --exclude electron --exclude react --exclude react-dom --require ./node_modules/react/dist/react.min.js:react --require ./node_modules/react-dom/dist/react-dom.min.js:react-dom -o ./dist/src/assets/renderer.js",
"minify": "cross-env NODE_ENV=production babel ./dist/src/assets/renderer.js -d ./",
"build": "run-s bundle minify"
},
"dependencies": {
"react": "^15.4.2",
"react-dom": "^15.4.2"
}
"devDependencies": {
"babel-cli": "^6.24.0",
"babel-preset-babili": "0.0.12",
"babel-preset-env": "^1.3.2",
"babel-preset-react": "^6.23.0",
"babel-register": "^6.24.0",
"babelify": "^7.3.0",
"browserify": "^14.1.0",
"cross-env": "^3.2.4",
"npm-run-all": "^4.0.2"
}
}
bundle
は env による ES.next の transpile と Browserify の bundle だけを実行。
minify
で bundle 結果となるファイルを minify して上書き保存。中間ファイルは不要。bundle 対象に dependencies
が含まれていた場合を考慮して NODE_ENV=production
を指定、npm によってはこの判定によりデバッグ処理を dead code 化して除去されることに期待した設計となっている。
これらを build
で npm-run-all により直列実行。結果、400KB まで肥大化したファイル サイズは約 318KB になって uglify-js より縮小された。Browserify で React 系を exclude/require しない、つまり babili で minify するようにしてみたのだが、めちゃくちゃ時間がかかったのとサイズが 400KB 超えしたので却下。
この方法で minify したファイルでアプリが正常に動作することも確認済み。
まとめ
ES.next に対応した minify ツールとして babili を試した。現時点のバージョンは v0.0.12 なので本番プロダクトに採用するのは不安があるものの、Babel ファミリーということもあって uglify-js 代替となることを期待している。
ES.next の transpiler としては babel-preset-env が本命となるだろう。しかし代表的な Web ブラウザー全種と Node が Modules 対応するのは相当に先の話となり、特に Web ブラウザー向けには Browserify や webpack などの bundler を採用することになるだろう。よって完全な minify を実行するためには transpile/bundle を経たコードを対象としなければならない。そのため uglify-js、babili のどちらも bundler と組み合わせるならビルドの最終工程で実行したほうがよい。
最後に用途別 minify 戦略をまとめる。
- 常に最新 ES.next を利用してあらゆる環境に対応したい
- env は標準設定、つまり latest にする
- ES.next はすべて ES5 相当に変換
- 環境によっては無駄な変換も含まれるが許容する
- 変換結果に ES.next が含まれないため minify は実績のある uglify-js 通常版を採用
- 対象環境を明示してなるべく無駄な変換をなくしたい
- env に対象環境を設定
- ES.next は設定に応じて最小の ES5 変換
- 変換結果に ES.next が含まれるため minify は babili か uglify-js harmony 版の二択
私としては現時点なら env 標準 + uglify-js を選ぶ。まだ babili は実績と品質面で不安がある。それと uglify-js harmony 版のなりゆきを見て判断したい。
Comments from WordPress
- mysticatea 2017-04-11T03:53:55Z
何点か。
-
browserify
にはグローバル トランスフォームのオプションがあり、node_modules
下を含めてすべてのファイルを変換することができます。例えばbrowserify test.js -t babelify -g [ babelify --presets babili --no-babelrc ] > test.min.js
ただし、この場合は Browserify が作る "つなぎ" の部分は Minify されません。
-
babili
は CLI コマンドも提供していますので、次のようにも書けます:browserify test.js -t babelify | babili > test.min.js
-
- アカベコ 2017-04-11T10:49:14Z
-g オプションは知りませんでした。これを前提にすると transpile されていない ES.next 使いまくりの状態で配布されてる npm も babel-preset-env で適切に変換できますね。配布側だけ最新 Node を下限にして利用側は指定なし、ただし browserify -g + babel-preset-env で transpile というのはよいかも。
つなぎの部分も変換となるとやはり babili 単体実行が必要で、CLI なら uglify-js と同じくパイプになるのですね。
細かなオプション指定することを想定すると本記事のように babe プロパティ (or babelrc) のほうがよさそうですが、babili が uglify-js 代替として十分になればそのまま CLI をすげ替えられるのはよいですね。