147
101

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-05-08

ゴールデンウィークの最中に、Facebookから「Prepack」が公開されました。

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

FacebookのPrepack

公式サイトのコード

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

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

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

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

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

使い方

ちょっと試すだけなら公式サイトの「TRY IT OUT」を利用するといいでしょう。

Prepackの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はこちら

スクリーンショット 2017-05-08 10.54.45.png

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

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はまだ初期段階なので、プロダクションに使う状態にはなってないよ)と紹介されているとおり、実務で利用できる段階にはなっていません。

例えば、documentwindowオブジェクトへアクセスするだけでエラーになったり(__assumeDataProperty()メソッドで定義すれば通過できる)、letconstはES5へトランスパイラされたりしません。~~ES6のアロー関数やclassはエラーになるので、~~BabelでES5にトランスパイラしてからPrepackを適用するのが良さそうです。

※Prepackも0.2.6から0.2.47に更新すると、classが利用可能になってました

他にも気になることがあったので、Tweetで紹介しておきます。

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は関数型プログラミングと相性が良いと考えてます。現時点では初期段階なので、長い目で進化を待つのがいいでしょう。

147
101
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
147
101

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?