JavaScript の Array 処理はどれが速いのか？ (2021 年 12 月)

JavaScript で `Array` に対して処理する方法はいくつかあります。
`Array` が提供しているメソッドとループで処理した場合にどちらが速いのか調べてみました。

※ 特に記載がない限り 2021 年 12 月 18 日時点の情報になります。

## 短い結論

普通の `for` ループが最速です。

とはいえ `Array` が提供するメソッドも十分に速いです。
可読性やコード量を考えると速度がとても重要かつ高頻度で呼ばれる処理以外は `Array` が提供するメソッドで問題ないと思います。

## 先行研究

2 年以上前の記事ですが次の記事でループ処理について調べられています。
それによると普通の `for` ループが最速だけど Babel や TypeScript は `for...of` を普通の `for` ループに展開するとのことでした。

https://qiita.com/shibukawa/items/4cae2a1410754d519232

ほかにも `Array` の処理について調べた記事がいくつかありました。 [^1] [^2]

[^1]: https://qiita.com/yukiB/items/dd74cb8ebcfb5babe874
[^2]: https://qiita.com/Hibikine_Kage/items/660392270c466b3f9805

## 測定方法

10000 個の数値が格納された `Array` を処理するテストコードを [JSBench.me](https://jsbench.me/) で作成しました。
テストコードは早期終了しないように条件を整えています。

- [`Array`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array) のメソッドを使用する。
- [`for`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for) ループでインデックスアクセスする。
- [`for...of`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for...of) ループでアクセスする。
- [`Array.prototype.forEach()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach) を使用する。

[`for...in`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/for...in) は配列での使用が推奨されていないため試していません。

測定環境は Windows 10 21H2 上の Chrome 96 および Firefox 95 と、 iOS 15.2 上の Safari です。

Node.js や Deno は試していませんが両方とも V8 を利用していますので Chrome とそれほど変わらないのではないかと思っています。

## 結果

:::note warn
結果はあくまでテストコードのものであり実際の処理を反映したものではないことにご注意ください。
:::

最も速かった方法を **太字** で、最も遅かった方法を _斜体_ にしています。
差異が 5 % 未満の場合は同率とみなしました。

iOS に比べて Windows 10 の数字が振るわないのは使用した PC が古いからです。

### Array.prototype.every()

[`Array.prototype.every()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/every) はすべての要素が条件を満たしているかどうかを boolean 値で返します。

※ `forEach()` は早期終了できないため本来この用途で使うべきではありません。

https://jsbench.me/uikx3amo0o

|             | Chrome 96          | Firefox 95         | Safari 15.2         |
| ----------- | ------------------ | ------------------ | ------------------- |
| `every()`   | _7380.3 ops/s_     | _10075.81 ops/s_   | 20740.33 ops/s      |
| `for`       | **124654.9 ops/s** | **75078.59 ops/s** | **160115.59 ops/s** |
| `for...of`  | 73325.97 ops/s     | 12734.04 ops/s     | 50587.57 ops/s      |
| `forEach()` | 9343.29 ops/s      | _10306.86 ops/s_   | _4067.92 ops/s_     |

普通の `for` ループが最速で `every()` との差は 7.5 ～ 16.9 倍程度あります。
`for...of` ループは `every()` より速いものの `for` との間には 1.7 ～ 5.8 倍程度の差があります。

### Array.prototype.filter()

[`Array.prototype.filter()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter) は条件を満たした要素による新しい配列を返します。

https://jsbench.me/jlkx4tjga0

|             | Chrome 96          | Firefox 95       | Safari 15.2        |
| ----------- | ------------------ | ---------------- | ------------------ |
| `filter()`  | _5118.88 ops/s_    | 3812.93 ops/s    | 12337.09 ops/s     |
| `for`       | **24883.59 ops/s** | **5260.2 ops/s** | **27659.75 ops/s** |
| `for...of`  | 23145.85 ops/s     | 4179.7 ops/s     | **27088.03 ops/s** |
| `forEach()` | 6770.83 ops/s      | _3569.15 ops/s_  | _3188.5 ops/s_     |

普通の `for` ループが最速で `filter()` との差は 1.4 ～ 4.9 倍程度あります。
`for...of` ループはブラウザにもよりますが `for()` に近い性能が出ています。

### Array.prototype.find()

[`Array.prototype.find()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find) は条件を満たした最初の要素を返します。

※ `forEach()` は早期終了できないため本来この用途で使うべきではありません。

https://jsbench.me/frkx67p0mh

|             | Chrome 96          | Firefox 95         | Safari 15.2         |
| ----------- | ------------------ | ------------------ | ------------------- |
| `find()`    | _8869.38 ops/s_    | _10437.32 ops/s_   | 17689.58 ops/s      |
| `for`       | **75320.49 ops/s** | **75468.88 ops/s** | **160571.43 ops/s** |
| `for...of`  | **73324.49 ops/s** | 12875.45 ops/s     | 50507.62 ops/s      |
| `forEach()` | _9098.93 ops/s_    | _10527.09 ops/s_   | _4063.55 ops/s_     |

普通の `for` ループが最速で `find()` との差は 7.2 ～ 9.1 倍程度あります。
`for...of` ループは Chrome では `for` ループと同等の性能が出ていますが Firefox / Safari では `for` との間に明らかな差がついています。

### Array.prototype.findIndex()

[`Array.prototype.findIndex()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex) は条件を満たした最初の要素のインデックスを返します。

※ `forEach()` は早期終了できないため本来この用途で使うべきではありません。

https://jsbench.me/99kx67xhl6

|               | Chrome 96           | Firefox 95         | Safari 15.2         |
| ------------- | ------------------- | ------------------ | ------------------- |
| `findIndex()` | 9270.52 ops/s       | 10886.56 ops/s     | 24088.21 ops/s      |
| `for`         | **124751.52 ops/s** | **74632.83 ops/s** | **157593.69 ops/s** |
| `for...of`    | 60863.4 ops/s       | 11657.37 ops/s     | 49029.13 ops/s      |
| `forEach()`   | _8300.89 ops/s_     | _8399.1 ops/s_     | _3812.51 ops/s_     |

普通の `for` ループが最速で `findIndex()` との差は 6.5 ～ 13.5 倍程度あります。
`for...of` ループは `findIndex()` より速いものの `for` との間には 2.0 ～ 6.4 倍程度の差があります。
また、 Firefox では `for...of` ループと `findIndex()` が近い結果になっています。

### Array.prototype.includes()

[`Array.prototype.includes()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/includes) は指定した値が含まれているかどうかを boolean 値で返します。

※ `forEach()` は早期終了できないため本来この用途で使うべきではありません。

https://jsbench.me/d8kx3b2hfi

|              | Chrome 96           | Firefox 95         | Safari 15.2         |
| ------------ | ------------------- | ------------------ | ------------------- |
| `includes()` | 63892.08 ops/s      | 53757.02 ops/s     | 123028.06 ops/s     |
| `for`        | **124989.14 ops/s** | **74253.94 ops/s** | **160479.43 ops/s** |
| `for...of`   | 73350.58 ops/s      | 12760.88 ops/s     | 50813.92 ops/s      |
| `forEach()`  | _9090.29 ops/s_     | _10187.45 ops/s_   | _3769.47 ops/s_     |

普通の `for` ループが最速で `includes()` との差は 1.3 ～ 2.0 倍程度あります。
`for...of` ループは Chrome では `includes()` より速いものの、 Firefox / Safari では `includes()` の方が倍以上速いという結果になりました。

### Array.prototype.indexOf()

[`Array.prototype.indexOf()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf) は指定した値と同じ最初の要素のインデックスを返します。

※ `forEach()` は早期終了できないため本来この用途で使うべきではありません。

https://jsbench.me/h2kx682gpl

|             | Chrome 96           | Firefox 95         | Safari 15.2        |
| ----------- | ------------------- | ------------------ | ------------------ |
| `indexOf()` | 63841.43 ops/s      | 61759.94 ops/s     | **243970.6 ops/s** |
| `for`       | **124977.31 ops/s** | **75421.91 ops/s** | 159873.61 ops/s    |
| `for...of`  | 60847.14 ops/s      | 11882.97 ops/s     | 48975.95 ops/s     |
| `forEach()` | _8288.36 ops/s_     | _8733.95 ops/s_    | _3986.41 ops/s_    |

Chrome / Firefox では普通の `for` ループが最速でしたが Safari は `indexOf()` の方が速い結果になりました。
`for...of` ループの結果も Chrome では `indexOf()` とほぼ同じ、 Firefox / Safari では `indexOf()` の方が 5 倍前後速いという結果になりました。

### Array.prototype.map()

[`Array.prototype.map()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/map) は与えられた関数をすべての要素に対して呼び出した結果を新しい配列として返します。

https://jsbench.me/3mkx676cd3

|             | Chrome 96          | Firefox 95        | Safari 15.2        |
| ----------- | ------------------ | ----------------- | ------------------ |
| `map()`     | 7328.94 ops/s      | **5719.84 ops/s** | _3198.75 ops/s_    |
| `for`       | **25419.7 ops/s**  | 5061.33 ops/s     | **27739.21 ops/s** |
| `for...of`  | **24455.61 ops/s** | 4001.65 ops/s     | **27471.53 ops/s** |
| `forEach()` | _6891.12 ops/_     | _3081.78 ops/s_   | _3219.1 ops/s_     |

Chrome / Safari では普通の `for` ループが 3.5 ～ 8.7 倍程度速いものの Firefox は `map()` の方が速い結果になりました。
`for...of` ループの結果も Chrome / Safari では `for` とほぼ同じですが Firefox では `for` の方が 1.3 倍前後速いという結果になっています。

### Array.prototype.reduce()

[`Array.prototype.reduce()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) は与えられた関数をすべての要素に対して呼び出した結果を返します。

https://jsbench.me/onkx67hsex

|             | Chrome 96           | Firefox 95         | Safari 15.2        |
| ----------- | ------------------- | ------------------ | ------------------ |
| `reduce()`  | 6279.79 ops/s       | _7563.36 ops/s_    | _3620.21 ops/s_    |
| `for`       | **124838.37 ops/s** | **62598.63 ops/s** | **16696.64 ops/s** |
| `for...of`  | 72855.98 ops/s      | 11517.46 ops/s     | 12516.1 ops/s      |
| `forEach()` | _4856.55 ops/s_     | 8562.24 ops/s      | _3592 ops/s_       |

普通の `for` ループが最速で `reduce()` との差は 4.6 ～ 19.9 倍程度あります。
`for...of` ループは `reduce()` より速いものの `for` との間には 1.3 ～ 5.4 倍程度の差があります。

### Array.prototype.some()

[`Array.prototype.some()`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/some) は条件を満たす要素が含まれているかどうか boolean 値で返します。

※ `forEach()` は早期終了できないため本来この用途で使うべきではありません。

https://jsbench.me/hmkx3bc8cl

|             | Chrome 96           | Firefox 95        | Safari 15.2         |
| ----------- | ------------------- | ----------------- | ------------------- |
| `some()`    | _9083.38 ops/s_     | _9889.48 ops/s_   | 22580.03 ops/s      |
| `for`       | **124672.56 ops/s** | **74082.2 ops/s** | **159535.44 ops/s** |
| `for...of`  | 73228.52 ops/s      | 12655.53 ops/s    | 50509 ops/s         |
| `forEach()` | _9092.02 ops/s_     | _10095.89 ops/s_  | _3891.24 ops/s_     |

普通の `for` ループが最速で `some()` との差は 7.1 ～ 13.7 倍程度あります。
`for...of` ループは `some()` より速いものの `for` との間には 1.7 ～ 5.9 倍程度の差があります。

## トランスパイル

前述の記事にも記載されていますが現代では書いた JavaScript をそのまま実行する場面は少なく、多くの場合はトランスパイルで変換されたコードを実行しています。
今回試したコードについてもどのようにトランスパイルされるのかを [Babel](https://babeljs.io/) および [TypeScript](https://www.typescriptlang.org/) のサイトで確認しましたがデフォルト設定では変換されませんでした。

前述の記事に記載されている `for...of` は ES2015 (ES6) で、最も新しい `Array.prototype.includes()` でも ES2016 (ES7) で追加された仕様ですので古いブラウザをサポートしない環境では不思議なことではないと思います。

## まとめ

次のことが確認できました。

- 多くの場合は普通の `for` ループによるインデックスアクセスが最速
- 関数呼び出しは高コスト
  - 引数に関数ではなく値を渡す場合は `for` ループとの差が小さくなる
- `forEach()` は安定して遅い (特に Safari)

冒頭の繰り返しになりますが可読性やコード量を考えると速度がとても重要かつ高頻度で呼ばれる処理以外は `Array` が提供するメソッドで問題ないと思います。