ゴールデンウィークの最中に、Facebookから「Prepack」が公開されました。
PrepackはJavaScriptのソースコードを最適化するツール。計算可能なことはコンパイル時にしてしまうことで、実行時の性能を向上させるというもの。
公式サイトのコード
適用前のコードがこれだとしたら
(function () {
function hello() { return 'hello'; }
function world() { return 'world'; }
global.s = hello() + ' ' + world();
})();
Prepackを適用するとこんな感じになります。
(function () {
s = "hello world";
})();
結果が計算できるようなら、事前に計算されます。例えば、文字列の結合や数値計算など。
これによって、コードの実行速度の向上が期待できます。
使い方
ちょっと試すだけなら公式サイトの「TRY IT OUT」を利用するといいでしょう。
ローカルに環境を構築するには
手元で使いたい場合は、Node.jsをインストールしたうえで、次の手順でセットアップします。利用するときは、コマンドライン(Windowsではコマンドプロンプト、macOSではターミナル)を使います。
インストール方法
npmでのインストール用コマンド
npm install prepack --save-dev
Yarnでのインストール用コマンド
yarn add prepack -D
実行方法
コマンドで指定
prepack ソースファイル --out 出力ファイル
ソースマップなどさまざまなオプションがあるのですが、詳しくは公式ドキュメント「Prepack · Partial evaluator for JavaScript」を見れば全部わかります。
どれだけ高速化できるか試してみた
検証デモのURLはこちら
ソースコード一式はこちら
GitHub : test-prepack-particle
Prepackを通してコードがどれだけ最適化されたか?
最適化前のJSコード
(function () {
const MAX_NUM = 10000; // パーティクルの個数
const STAGE_W = 465;
const STAGE_H = 465;
const FRICTION = 0.96;
const ACC_VALUE = 50;
/**
* パーティクルクラスです。
* @param {number} x
* @param {number} y
* @constructor
*/
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
}
}
function updateParticlePosition(particle, gravityX, gravityY) {
const diffX = gravityX - particle.x;
const diffY = gravityY - particle.y;
const acc = ACC_VALUE / (diffX * diffX + diffY * diffY);
const accX = acc * diffX;
const accY = acc * diffY;
particle.vx += accX;
particle.vy += accY;
particle.x += particle.vx;
particle.y += particle.vy;
particle.vx *= FRICTION;
particle.vy *= FRICTION;
if (particle.x > STAGE_W)
particle.x = 0;
else if (particle.x < 0)
particle.x = STAGE_W;
if (particle.y > STAGE_H)
particle.y = 0;
else if (particle.y < 0)
particle.y = STAGE_H;
}
class World {
constructor() {
this.particleList = [];
}
init() {
// パーティクルの初期化
for (let i = 0; i < MAX_NUM; i++) {
const p = new Particle(
Math.random() * STAGE_W,
Math.random() * STAGE_H,
);
this.particleList.push(p);
}
};
update(gravityX, gravityY) {
this.particleList.forEach((particle) => {
updateParticlePosition(particle, gravityX, gravityY);
});
return this.particleList;
};
}
global.World = World;
})();
最適化後のJSコード
(function () {
var _$0 = this;
var _A = function (particle, gravityX, gravityY) {
const diffX = gravityX - particle.x;
const diffY = gravityY - particle.y;
const acc = 50 / (diffX * diffX + diffY * diffY);
const accX = acc * diffX;
const accY = acc * diffY;
particle.vx += accX;
particle.vy += accY;
particle.x += particle.vx;
particle.y += particle.vy;
particle.vx *= 0.96;
particle.vy *= 0.96;
if (particle.x > 465) particle.x = 0;else if (particle.x < 0) particle.x = 465;
if (particle.y > 465) particle.y = 0;else if (particle.y < 0) particle.y = 465;
};
var _1 = class {
constructor() {
this.particleList = [];
}
init() {
// パーティクルの初期化
for (let i = 0; i < 10000; i++) {
const p = new _8(Math.random() * 465, Math.random() * 465);
this.particleList.push(p);
}
}
update(gravityX, gravityY) {
this.particleList.forEach(particle => {
_A(particle, gravityX, gravityY);
});
return this.particleList;
}
};
var _8 = class {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
}
};
_$0.World = _1;
}).call(this);
定数として宣言した数値が、Prepackを通すことでハードコーディングされた形になっています。
このサンプルだと実行速度の向上はあまりわかりませんね…w 計算結果をハードコーディングすることはFlashの時代から最適化の一つの手法として知られていました(可読性が低くなるので、限界に挑戦するときだけ採用してました)。もっと複雑なコードだと恩恵が受けれるかもしれません。
Prepackの所感
公式サイトに「Prepack is still in an early development stage and not ready for production use just yet」(Prepackはまだ初期段階なので、プロダクションに使う状態にはなってないよ)と紹介されているとおり、実務で利用できる段階にはなっていません。
例えば、document
やwindow
オブジェクトへアクセスするだけでエラーになったり(__assumeDataProperty()
メソッドで定義すれば通過できる)、let
やconst
はES5へトランスパイラされたりしません。~~ES6のアロー関数やclass
はエラーになるので、~~BabelでES5にトランスパイラしてからPrepackを適用するのが良さそうです。
※Prepackも0.2.6から0.2.47に更新すると、class
が利用可能になってました
他にも気になることがあったので、Tweetで紹介しておきます。
Prepackの所感
— 池田 泰延 (@clockmaker) 2017年5月6日
①どんなJSでも最適化してくれるツールではない(ルールに沿う必要がある)
②windowやdocumentなどの定義はない(独自に定義しないとエラーになる)
③導入で性能向上を体感できるプロジェクトは限られそう
④試しつつロードマップをよく読むのが良い
Closure Compiler
類似の技術としてClosure Compilerがあります。Closure Compilerは容量の最適化に重きを置いていて、Prepackは実行速度に重きを置いています。
ライセンス
Facebook製ということもあり、Prepackは「特許条項付きBSDライセンス」という恐ろしいライセンスになっています。かつてReactが採用していたライセンスでバッシングされてましたね。
Prepack is BSD-licensed. We also provide an additional patent grant.
詳しくは記事「Facebookの特許条項付きBSDライセンスが炎上している件について | こんぴゅ | note」が参考になるでしょう。
まとめ
JavaScriptのパフォーマンス厨としては、将来性が気になるプロジェクトです。Prepackは関数型プログラミングと相性が良いと考えてます。現時点では初期段階なので、長い目で進化を待つのがいいでしょう。