logo Ribbit's works

スプレッド構文とconcat、どちらを使うべきか【javascript】

#javascript
にメンテナンス済み
記事のトップ画像

先に結論

可読性 ➡ スプレッド構文

速度 ➡ JavaScript エンジンに依る

が結論です。

もともとスプレッド構文自体が、以前よりも強力で柔軟な配列リテラルを構築するために用意されたもので、使用できる環境であれば、スプレッド構文が推奨されています

スプレッド構文を使用しない場合、既存の配列を一部として使用して新しい配列を作成するには、配列リテラル構文は十分ではなく、push(), splice(), concat() などを組み合わせて使う高圧的なコードを使用しなければなりません。 スプレッド構文 - Javascript | MDN

速度については検証の結果、以下のようになりました。

  1. JavaScript エンジン: V8(Chrome, Edge, Node.js), SpiderMonkey(Firefox)

    • concat の方が 3 倍速い
  2. JavaScript エンジン: JavaScriptCore(Safari, bun)

    • わずかにスプレッド構文の方が速い

Chrome や Edge、Node.js などの V8 エンジンを使用している場合は、concatが圧倒的に速いです。

Safari や bun などの JavaScriptCore エンジンを使用している場合、大きな差は見られませんでしたが、スプレッド構文の方が若干速いようです。

Firefox に採用されている SpiderMonkey エンジンでは、V8 エンジンとほとんど同じ結果になりました。

操作デモ

ご利用の環境で実際に速度を検証できます。

CONCATスプレッド構文それぞれのボタンをクリックすると、それぞれの記法で 10 万の要素を持つ配列を結合し、100 回実行した合計時間が 10 回分表示されます。

速度の検証

V8 エンジン

まず最初に、ブラウザとして最も広く使われている Google Chrome で採用されている V8 エンジンで検証します。

Chromium ベースのブラウザは全て V8 エンジンを採用しているため、Edge や Opera、Brave でも同様の結果が得られると思われます。

検証環境と検証方法

環境として Node.js を使用し、実行したコードはこの記事の最後に記載していますが、概要は以下の通りです。

  • 配列数: 10000 ~ 201000
  • 繰り返し回数: 100

上記の条件で、配列数それぞれについて 10 回ずつ実行し、その平均値を取りました。

また、実行タイミングによる物理的なリソースの影響を最小限に抑えるため、それぞれの記法を交互に実行しています。

検証結果

concat
spread
100005100001010000151000020100000ms250ms500ms750ms

結果を見ると、concat の方がスプレッド構文よりも 3 倍速く、その差は配列数が増えるほど大きくなっていることがわかります。

今回は最小でも 10000 という非常に要素数の多い配列を使用していますが、速度を考慮する場合はconcatを使用した方が良いという結果となりました。

JavaScriptCore エンジン

続いて、Safari や bun などで採用されている JavaScriptCore エンジンで検証します。

検証環境と検証方法

環境として bun を使用し、実行したコードは前述した V8 エンジンのものと同じです。

  • 配列数: 10000 ~ 201000
  • 繰り返し回数: 100

上記の条件で、配列数それぞれについて 10 回ずつ実行し、その平均値を取りました。

検証結果

concat
spread
100005100001010000151000020100000ms150ms300ms450ms

V8 エンジンとは異なり、JavaScriptCore エンジンではスプレッド構文の方が若干速い結果となりました。

ただし、その差は非常に小さく、実用上は無視できる程度です。

検証に使用したコード

今回検証に使用したコードは以下の通りです。

TypeScript で記述したものをトランスパイルして実行しました。

import fs from 'fs';

/** 配列の要素数の基準値 */
const BASIS_COUNT = 10000;
/** 試行回数 */
const ATTEMPT_COUNT = 10;
/** 1回の試行でのループ回数 */
const LOOP_COUNT = 10;

/** 配列の結合方法の名称と関数 */
interface JoinMethod {
  name: string;
  func: (a: number[], b: number[]) => number[];
}

/** 配列を`concat`で結合する */
const concatMethod: JoinMethod = {
  name: 'concat',
  func: (a, b) => a.concat(b),
};

/** 配列をスプレッド構文で結合する */
const spreadMethod: JoinMethod = {
  name: 'spread',
  func: (a, b) => [...a, ...b],
};

/** 測定対象の配列結合方法 */
const joinMethods: JoinMethod[] = [concatMethod, spreadMethod];

/** 配列の平均値を計算する */
const calculateAverage = (arr: number[]) =>
  arr.length === 0 ? 0 : arr.reduce((acc, cur) => acc + cur, 0) / arr.length;

/** 測定結果を格納するインターフェース */
interface MeasurementResult {
  arrayLength: number;
  [methodName: string]: number;
}

/** 測定結果を格納する配列 */
const measurementResults: MeasurementResult[] = [];

// 配列の長さを変化させて測定を行う
for (let amount = 1; amount <= 201; amount += 5) {
  const arrayLength = amount * BASIS_COUNT;
  const result: MeasurementResult = { arrayLength };

  // 各結合方法で測定を行う
  for (const { name, func } of joinMethods) {
    const executionTimes: number[] = [];

    // 一定回数試行して実行時間を計測する
    for (let i = 0; i < ATTEMPT_COUNT; i++) {
      const arrays = new Array(2).fill(new Array(arrayLength).fill(0));
      const start = performance.now();

      for (let j = 0; j < LOOP_COUNT; j++) {
        func(arrays[0], arrays[1]);
      }

      const executionTime = performance.now() - start;
      executionTimes.push(executionTime);
    }

    result[name] = calculateAverage(executionTimes);
  }

  measurementResults.push(result);
}

fs.writeFileSync('measurement-results.json', JSON.stringify(measurementResults, null, 2));

まとめ

本来スプレッド構文は、配列リテラルを柔軟に構築するために用意されたもので、使用できる環境であればスプレッド構文の方が良い。とされていますが、 パフォーマンスを考慮すると他の記法が選択肢に入ってしまうことが分かりました。

これでは本末転倒ですし、エンジンが解釈する際の最適化の仕方によっては改善の余地があるため、ほとんどの場合はスプレッド構文を使用すれば良いでしょう。

ただし、速度を重視する場合は、使用しているエンジンによってはconcatの方が速いこともあるため、その場合はconcatを使用することも検討してみてください。

以上がスプレッド構文とconcatの比較についての検証結果となります。

参考

このページで使用した関数や記法の詳細は以下のリンクを参照してください。