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

とある事情で[EventEmitter3](https://www.npmjs.com/package/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では再現しない

### 事象が発生したコード

```js
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
<img width="146" alt="ee.png" src="https://qiita-image-store.s3.amazonaws.com/0/142643/e321f033-d9ad-b6b6-d2f0-5a001cfc6d79.png">

productionモードだと10msオーバー
<img width="145" alt="mini_ee.png" src="https://qiita-image-store.s3.amazonaws.com/0/142643/722d170a-646a-23b3-0e9e-74ba96d3f9a1.png">

圧倒的劣化っ・・・・・・！

## productionモードの何が原因か？

productionモードは本番環境向けに最適化プラグインなどを適用してくれる設定ですね。[Mode: production](https://webpack.js.org/concepts/mode/#mode-production)

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

`minimize: true`

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

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

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

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

### 再現するコード

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

```js
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を多少クラスらしくしてあげました。

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

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

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

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

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

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

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

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

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

### 劣化するコード

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

```js
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`として定義されるようになった。

```js
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`として定義されている。

```js
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()`されている。

```js
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()`でのインスタンス生成の差分が怪しそうです。

```js
  on(e) {
    this.listeners.push(new function (e) { 
      this.fn = e
    }(e))
  }
```

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

## CodePenで確認

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

<p data-height="265" data-theme-id="0" data-slug-hash="aQBmzv" data-default-tab="js,result" data-user="zprodev" data-pen-title="aQBmzv" class="codepen">See the Pen <a href="https://codepen.io/zprodev/pen/aQBmzv/">aQBmzv</a> by zprodev (<a href="https://codepen.io/zprodev">@zprodev</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

<p data-height="265" data-theme-id="0" data-slug-hash="QJGKjz" data-default-tab="js,result" data-user="zprodev" data-pen-title="QJGKjz" class="codepen">See the Pen <a href="https://codepen.io/zprodev/pen/QJGKjz/">QJGKjz</a> by zprodev (<a href="https://codepen.io/zprodev">@zprodev</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

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

## まとめ

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