JavaScriptを高速化するFacebook製Prepackを試してみた

ゴールデンウィークの最中に、Facebookから「[Prepack](https://prepack.io/)」が公開されました。

PrepackはJavaScriptのソースコードを最適化するツール。計算可能なことはコンパイル時にしてしまうことで、実行時の性能を向上させるというもの。

<img width="1084" alt="FacebookのPrepack" src="https://qiita-image-store.s3.amazonaws.com/0/2544/aac39e5f-c9d4-d1dc-8f40-0106cc40a161.png">




## 公式サイトのコード

適用前のコードがこれだとしたら

```js
(function () {
  function hello() { return 'hello'; }
  function world() { return 'world'; }
  global.s = hello() + ' ' + world();
})();
```

Prepackを適用するとこんな感じになります。

```js
(function () {
  s = "hello world";
})();
```

結果が計算できるようなら、事前に計算されます。例えば、文字列の結合や数値計算など。
これによって、コードの実行速度の向上が期待できます。


## 使い方

ちょっと試すだけなら公式サイトの「[TRY IT OUT](https://prepack.io/repl.html)」を利用するといいでしょう。

<img width="1138" alt="PrepackのTry it out" src="https://qiita-image-store.s3.amazonaws.com/0/2544/b48e4358-1af7-4d98-31c0-de340a84fe2b.png">


### ローカルに環境を構築するには


手元で使いたい場合は、Node.jsをインストールしたうえで、次の手順でセットアップします。利用するときは、コマンドライン(Windowsではコマンドプロンプト、macOSではターミナル)を使います。


#### インストール方法

npmでのインストール用コマンド

```bash
npm install prepack --save-dev
```

Yarnでのインストール用コマンド

```bash
yarn add prepack -D
```


#### 実行方法

コマンドで指定

```bash
prepack ソースファイル --out 出力ファイル
```

ソースマップなどさまざまなオプションがあるのですが、詳しくは公式ドキュメント「[Prepack · Partial evaluator for JavaScript](https://prepack.io/getting-started.html)」を見れば全部わかります。

## どれだけ高速化できるか試してみた

検証デモのURLはこちら

- [最適化前のデモ](https://ics-ikeda.github.io/test-prepack-particle/src/)
- [最適化後(Prepack適用)のデモ](https://ics-ikeda.github.io/test-prepack-particle/www/)

<img width="466" alt="スクリーンショット 2017-05-08 10.54.45.png" src="https://qiita-image-store.s3.amazonaws.com/0/2544/d773512f-edcc-f3ad-1684-f732004a5d27.png">




ソースコード一式はこちら

[GitHub : test\-prepack\-particle](https://github.com/ics-ikeda/test-prepack-particle)

### Prepackを通してコードがどれだけ最適化されたか？

最適化前のJSコード

https://github.com/ics-ikeda/test-prepack-particle/blob/master/src/particle.js


```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コード

https://github.com/ics-ikeda/test-prepack-particle/blob/master/www/particle.js

```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で紹介しておきます。

<blockquote class="twitter-tweet" data-conversation="none" data-lang="ja"><p lang="ja" dir="ltr">Prepackの所感<br>①どんなJSでも最適化してくれるツールではない（ルールに沿う必要がある）<br>②windowやdocumentなどの定義はない（独自に定義しないとエラーになる）<br>③導入で性能向上を体感できるプロジェクトは限られそう<br>④試しつつロードマップをよく読むのが良い</p>&mdash; 池田 泰延 (@clockmaker) <a href="https://twitter.com/clockmaker/status/860790239140302848">2017年5月6日</a></blockquote>

## Closure Compiler

類似の技術として[Closure Compiler](https://developers.google.com/closure/compiler/)があります。Closure Compilerは容量の最適化に重きを置いていて、Prepackは実行速度に重きを置いています。


## ライセンス

Facebook製ということもあり、Prepackは「特許条項付きBSDライセンス」という恐ろしいライセンスになっています。かつて[React](https://facebook.github.io/react/)が採用していたライセンスでバッシングされてましたね。

> Prepack is BSD-licensed. We also provide an additional patent grant.

詳しくは記事「[Facebookの特許条項付きBSDライセンスが炎上している件について \| こんぴゅ \| note](https://note.mu/konpyu/n/nc0d2f49676ba)」が参考になるでしょう。


## まとめ

JavaScriptのパフォーマンス厨としては、将来性が気になるプロジェクトです。Prepackは関数型プログラミングと相性が良いと考えてます。現時点では初期段階なので、長い目で進化を待つのがいいでしょう。