8
3

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.

Webpackのproductionモードでパフォーマンスが劣化する可能性について

Posted at

とある事情でEventEmitter3の処理速度を測定していた際、不可解な現象に遭遇しました。

WebpackのproductionモードでバンドルするとChromeでの性能が著しく劣化する

この原因について調査したので共有します。

結論から

  • classのインスタンス生成をある形式で実装すると、ChromeやFirefoxでインスタンス参照時のパフォーマンスが劣化する(っぽい)
  • productionモードで使用されるUglifyjsWebpackPluginによって、EventEmitter3の内部コードがその形式に変換されていた
  • レアケースですが CommonJS + uglifyjs では要注意

環境

eventemitter3 : 3.1.0
webpack : 4.17.1
Chrome : 70

事象

  • productionモードでバンドルするとパフォーマンスが劣化した
  • ChromeとFirefoxで再現した
  • Safariでは再現しない

事象が発生したコード

import { EventEmitter } from 'eventemitter3';
const eventEmitter = new EventEmitter();

// 'test'イベントに10000個のリスナーを登録
for (let i = 0; i < 10000; i++){
  eventEmitter.on('test', () => { });
}

const loop = () => {
  const before = performance.now();
  // 毎フレーム'test'イベント発火
  eventEmitter.emit('test');
  // 処理時間をログ出力
  console.log(performance.now() - before);
  requestAnimationFrame(loop);
};
loop();

1つのイベントに10000件のリスナーを登録して、毎フレーム発火させる処理です。
イベント発火時の処理時間を計測してログに出力しています。

計測結果

developモードでは0.2〜0.3ms
ee.png

productionモードだと10msオーバー
mini_ee.png

圧倒的劣化っ・・・・・・!

productionモードの何が原因か?

productionモードは本番環境向けに最適化プラグインなどを適用してくれる設定ですね。Mode: production

適用されるプラグインのいずれかに原因があると考えて、developモードに設定しつつoptimizationプロパティを手動で切り替えて調査した結果・・・

minimize: true

が原因だとわかりました。

UglifyjsWebpackPluginを使用してコードをminifyしてくれる設定ですね。

どんなコードをminifyすると再現するのか?

次にEventEmitter3をバラしながら原因のコードを探りました。

再現するコード

EventEmitter3の実装を簡易化した以下のコードで、minify時のみ性能劣化を確認しました。

function Listener(fn) {
  this.fn = fn;
}
class EventEmitter {
  constructor() {
    this.listeners = [];
  }
  on(fn) {
    this.listeners.push(new Listener(fn));
  }
  emit() {
    for (let i = 0; i < this.listeners.length; ++i){
      this.listeners[i].fn();
    }
  }
}

const eventEmitter = new EventEmitter();
for (let i = 0; i < 10000; i++){
  eventEmitter.on(() => { });
}

const loop = () => {
  const before = performance.now();
  eventEmitter.emit();
  console.log(performance.now() - before);
  requestAnimationFrame(loop);
};
loop();

再現しないコード

色々試した中で劣化しない書き方もいくつか見えました。

パターン1: Listenerクラスにprototypeを定義

Listenerを多少クラスらしくしてあげました。

function Listener(fn) {
  this.fn = fn;
}
Listener.prototype.hoge = () => { };

パターン2: ListenerクラスをES2015構文で書く

ES2015構文で明確にclassとして定義します。

class Listener {
  constructor(fn) {
    this.fn = fn;
  }
}

パターン3: Listenerがクラスである必要ないでしょ

コンストラクタだけのクラスならobjectで事足りますよね

// this.listeners.push(new Listener(fn));
this.listeners.push({ fn: fn });

minify後の差分はどうなっているのか?

再現するコード、再現しないコードのminify後のコードを見てみました。

劣化するコード

Listenerクラスの定義が無くなった。
this.listeners.push()時に都度定義して即時実行されている。

const n = new class {
  constructor() {
    this.listeners = []
  }
  on(e) {
    this.listeners.push(new function (e) { 
      this.fn = e
    }(e))
  }
  emit() {
    for (let e = 0; e < this.listeners.length; ++e) this.listeners[e].fn()
  }
};
for (let e = 0; e < 1e4; e++) n.on(() => {});
const r = () => {
  const e = performance.now();
  n.emit(), console.log(performance.now() - e), requestAnimationFrame(r)
};
r()

劣化しないパターン1

Listenerクラスがfunction nとして定義されるようになった。

function n(e) {
  this.fn = e
}
n.prototype.hoge = (() => {});
const r = new class {
  constructor() {
    this.listeners = []
  }
  on(e) {
    this.listeners.push(new n(e))
  }
  emit() {
    for (let e = 0; e < this.listeners.length; ++e) this.listeners[e].fn()
  }
};
for (let e = 0; e < 1e4; e++) r.on(() => {});
const o = () => {
  const e = performance.now();
  r.emit(), console.log(performance.now() - e), requestAnimationFrame(o)
};
o()

劣化しないパターン2

Listenerクラスがclass nとして定義されている。

class n {
  constructor(e) {
    this.fn = e
  }
}
const r = new class {
  constructor() {
    this.listeners = []
  }
  on(e) {
    this.listeners.push(new n(e))
  }
  emit() {
    for (let e = 0; e < this.listeners.length; ++e) this.listeners[e].fn()
  }
};
for (let e = 0; e < 1e4; e++) r.on(() => {});
const o = () => {
  const e = performance.now();
  r.emit(), console.log(performance.now() - e), requestAnimationFrame(o)
};
o()

劣化しないパターン3

ただのobjectとしてthis.listeners.push()されている。

const n = new class {
  constructor() {
    this.listeners = []
  }
  on(e) {
    this.listeners.push({
      fn: e
    })
  }
  emit() {
    for (let e = 0; e < this.listeners.length; ++e) this.listeners[e].fn()
  }
};
for (let e = 0; e < 1e4; e++) n.on(() => {});
const r = () => {
  const e = performance.now();
  n.emit(), console.log(performance.now() - e), requestAnimationFrame(r)
};
r()

怪しい差分は?

on()でのインスタンス生成の差分が怪しそうです。

  on(e) {
    this.listeners.push(new function (e) { 
      this.fn = e
    }(e))
  }
function n(e) {
  this.fn = e
}
//省略
  on(e) {
    this.listeners.push(new n(e))
  }

CodePenで確認

怪しい箇所にあたりがついたのでCodePenで再現コードを書いてみました。

See the Pen aQBmzv by zprodev (@zprodev) on CodePen.

See the Pen QJGKjz by zprodev (@zprodev) on CodePen.

自分の環境では1つ目のコードで劣化が確認できますが、みなさんの環境ではどうでしょうか?

まとめ

classのインスタンス生成の仕方によって、参照時(?)のパフォーマンスが劣化するという事象が確認できました。
珍しいケースだとは思いますが、パフォーマンスが求められる場面で CommonJS + uglifyjs は要注意かもしれません。

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?