🤖

JavaScript String文字列結合、Array.join、+=演算子、+演算子

に公開

JavaScriptのStringリテラル、Stringクラスの文字列結合について、2019年ごろの記事はありますが、2026年現在の最新の情報はいまいち、つかみにくい感じです。

知識の最新化と、何が本当らしいか、少し検証しつつ、記事に起こします。

AI、Grokとともに調査をしますが、記事を書いているのは私です。
まず、もともと他のツールの高速化ができないか検討していて、そのひとつが大量データによる文字列処理の連結データが重いのではないかという可能性についてです。
Array.joinによる高速化はだいぶ前には、すでに広く行われていて、常識ではありました。

2026年現在、はるか昔は低速であったらしいString += StringString + Stringの高速化、最適化は進んでおり、かなりのデータ量でも高速に動作するようです。

今回、試せるようにコードペンを使ってみました。

JSベンチ実行とコード

function $(id){
  return document.getElementById(id);
}
function myTestString(){
  let output = '';
  $('result').innerText = '実行開始';
  for(let i = 0; i < 5; i++){
    output += testStringA(i);
    output += ' ';
    output += testStringB(i);
    output += '\n';
  }
  $('result').innerText = output;
}
function testStringA(count){
  const timeOld = performance.now();
  let str = '';
  for(let i = 0; i < 100000; i++){
    str += `${i}行目です。文字列結合のテスト\n`;
  }
  $('result').innerText = str;
  const timeNew = performance.now();
  const msec = Math.floor(timeNew - timeOld);
  const output = `A: ${count} : ${msec} ms`;
  return output;
}
function testStringB(count){
  const timeOld = performance.now();
  let str = '';
  const array = [];
  for(let i = 0; i < 100000; i++){
    array.push(`${i}行目です。文字列結合のテスト\n`);
  }
  $('result').innerText = array.join('');
  const timeNew = performance.now();
  const msec = Math.floor(timeNew - timeOld);
  const output = `B: ${count} : ${msec} ms`;
  return output;
}
$('myFire').addEventListener('click', () => myTestString());
<h1>Stringクラス連結テスト</h1>
<form>
  <button type="button" id='myFire'>
    テスト実行
  </button>
</form>
<div id="result"></div>

コードペンでは、よく分からないのですが、bodyの中身だけ書いて、外側はテンプレートに従うみたいなので、とりあえず、いきなりbodyの内側を書いてあります。

Firefox 149.0.2 / Windows 11 x64
A: 0 : 74 ms B: 0 : 136 ms
A: 1 : 133 ms B: 1 : 137 ms
A: 2 : 147 ms B: 2 : 143 ms
A: 3 : 150 ms B: 3 : 132 ms
A: 4 : 184 ms B: 4 : 153 ms

Chrome 147 / Windows 11 x64
A: 0 : 51 ms B: 0 : 59 ms
A: 1 : 55 ms B: 1 : 45 ms
A: 2 : 44 ms B: 2 : 45 ms
A: 3 : 45 ms B: 3 : 45 ms
A: 4 : 43 ms B: 4 : 42 ms

Firefox結果

Aが+=演算子によるものです。
BがArray.pushおよびArray.joinです。

Firefoxでの実行結果は以上の通りです。
ちなみに10msくらいでは誤差が大きい可能性があるので、100msくらいになるようにループ数を調整しました。
performance.nowの分解能はmsより高い1未満のはずなので、1ms以下でも精度が出るはずなのですが、保守的な保険です。
メモリー使用量はGC(ガベージコレクション)にかなり影響を与えるので、文字列の長さによっては、だいぶ結果が異なる可能性があります。

Chrome結果

Chromeのほうが、このテストではちょっと高速ですね。
ただし、他の大量テキスト処理のテストをしたところ、Firefoxでは高速だったもののChromeではGCか何かが秒単位で重いことがあったので、エッジケースでは、どちらがは有利かは状況で違いそうです。
ちなみにメインメモリーは64GBのマシンなので、メモリーそのものが足りない可能性は低いです。
そもそも現実的な処理では、テキスト処理と画面表示に回すテキスト量からして、GB単位になることはほぼないと思います。
まあ、AI処理とかやるとその限りではない可能性もありますが、今回はスルーしておきます。

実行順による速度差がある可能性があるので、A、Bをループで回して、さらに交互に計測しました。
先に実行したほうが、平均的に速いとかは、よくある現象です。

寄り道

Stringオブジェクトには、通常、昔のC++のCOWのような最適化の仕組みがあります。
COWはCopy On writeといって、単純なものは参照カウンタなどでデータ共有をして、コピーを抑制する機能です。
しかしC++ではマルチスレッドによる複雑なロックや依存関係などの問題があり、COWは禁止になり、基本的にbasic_stringでは、それぞれ別のメモリーが割り当てられてコピーが必ず発生します(C++には、かわりに汎用の別の仕組みでコピーを減らす言語機能が導入されています)。
このコピー感覚でJavaScriptのStringを考えていると、+=+演算子は、計算するたびに新規の全コピーが発生するように「錯覚」するため、遅いと考えられがちです。
しかしJavaScriptのStringオブジェクトはイミュータブルの不変オブジェクトなので、データ共有が可能となり、最適化されているのが2026年現在の状況です。
これは、単純に参照カウンタの同一値のオブジェクトだけではなく、連結文字列の構造状況もメモリー上に記録されて、最適化に使われています。
そのため、単純に+=+で書いても、かなりのケースで高速に動作します。

つまり、生成された文字列の連結を、連結情報だけで済ませて、文字列そのもののコピーは必要になるまで遅延させる仕組み、だそうです。

しかし、連結した文字列を外部に渡すときには、内部的にすべてを連結する、仮に「リニア化」と呼びましょうか、が必要になります。
その一つが、innerTextinnerHTMLtextContentconsole.logといったDOM操作や外部との橋渡し操作です。
ということで、今回のテストケースでは、意図的にタイマーを観測する前に$('result').innerText = str;という行を挟んでいます。実際にはレンダリングされず、画面は更新されませんが、内部的にはStringの実データが渡されているはずなので、ここでStringの構築作業が必要なはずです。

Aだけすべて処理のあとBだけすべて処理するベンチマークをしている人も見かけますが、特に重い処理において、後のほうがCPU温度による低下や、GCの負荷を考えると、後半のほうが平均して不利な傾向があります。
多少の速度差は、単純ではなく、そういう環境上の問題で該当部分の実装だけでは決まらない部分があります。
古代のCPUは実行速度も一定ですが、現代のCPUのほとんどは実行速度が実行時でかなりの差があり、さらに割り当てられたCPUコア(Eコア、Pコアなど参考)でも速度が普通に違います。ターボブーストなどの加速機能もあるので、状況は複雑で、計測しても微妙なこともありそうです。

それから、挿入データを固定長文字列で計測してるコードもよく見かける、と思います。
しかし、固定長文字列は共有が可能という視点があるため、最適化されて高速になりやすいです。
実務では、そもそも大量データで中身が同じということはきわめて稀なので、ベンチマークとしての実用性としては、現代では疑問がありそうです。

ということで、今回のベンチマークコードでは、行番号を含んだ行データを生成して、それをpushなり+=していくコードを採用しました。
これで、少なくともデータ共有は限定的になるはずです。
もしかしたら、回数の数字部分だけ別メモリーで、あとは共有されている可能性はあります。

Arrayの事前確保

ArrayはC++のvector.reserve同様、事前にバッファ確保をしたほうがパフォーマンスが良好だとされていますが、今回は採用していません。
例えば、テキスト処理で、最初に全テキストをallText.split('\n')Arrayにした後、行ごと処理を回しつつ、出力用の別のArraypushしていくスタイルであれば、配列数は行数のはずなので、事前確保が可能だと考えられます。

ただし、実務上、何行くらい上限で処理するか、は考えたほうがいいと思います。
ログ解析とかだと、とんでもないことがありますが、あまりブラウザ上のJavaScriptでやることはないと思います。

function myProcess1(text){
  const lines = text.split('\n');
  const output = [];
  text = undefined; // 捨てる
  for(const line of lines){
     output.push(someProcess(line));
  }
  // output String
  return output.join(''); 
}

今主流の略記法は、[]での宣言方法(リテラル記法)で、前後のコードを合わせるとこんな感じです。
それで、事前確保するには「new Array(arrayLength);」というタイプのコンストラクタが使えます。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/Array

function myProcess2(text){
  const lines = text.split('\n');
  text = undefined; // 捨てる
  const output = new Array(lines.length);
  for (let i = 0; i < lines.length; i++) {
    output[i] = someProcess(lines[i]);
  }
  // output String
  return output.join(''); 
}

ただし、このコンストラクターの表記法は、知らないとちょっとびっくりしますね。
普通は中身を指定することが多いのがコンストラクターなので、長さは例外的です。
あと、この混乱があるので、単純に1個だけ挿入してnewしたいときに、ハマりやすいというポイントです。
この方法では、C++でいうvector::reserveとは異なり、すでにArraylength長の実配列が構築されているため、pushが使えないという制限があります。
そのため、=による代入操作がほぼ必須になります。

それで、肝心の高速化ですが、体感であまり変わらないか、小さな改善にとどまるというのが、Grok君の見立てのようです。
私の見解では、C++の実データを保持するタイプのstd::vectorのようなモノとは異なり、基本的にJava/C#系はポインター参照の形なので、データのディープコピーが発生しないため、いうほどパフォーマンスに影響しないということのようです。
もし配列が効率的だとするなら、「8byte * 要素数」が全体サイズで、容量もメモリーコピーも他の重い部分と比べて、軽量なほうです。
例えば、10万行=100K行でも、800KBでしかありません。
それに対して、文字列全体は5MBなど、数倍以上になります。

余談:テンプレートリテラル

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals
MozillaのMDN見ればだいたい書いてあるので、概略だけ書きますが、Markdownみたいに、バッククオート記号`を使った新しい文字列表記方法です。

old_string_concat.js
str += i + '行目です。' +
    `文字列結合のテスト\n`;

こういう風に、行末に+を書いて、連結していくか、行ごとに+=で結合していくか、考えることになります。
後ろで +結合したほうが毎回strと書かなくていいですが、単一行の命令が複数行に渡っているので、ちょっと見通しは低めかもしれません。
あと単純に" + " の部分が記号が連続して見づらいことがあります。

new_string_template_literal.js
  str += `${i}行目です。
文字列結合のテスト\n`;

こういう風にリテラル文字列中に変数名が埋め込んであり、それを実行時に解釈してくれる、というものです。\nを使える上に、普通の文字列ではできない「リテラル内での改行」も採用しています。
これにより、行ごとの固定文字列を演算子使いながら、連結していくのは不要になりました。

このリテラル表記は特徴として「可読性が高い」と言われていて、推奨されていると考えていいと思います。

おわりに

IEもはるか遠い過去になり、一時期はNetscape Navigatorからブランド変更されたMozilla Firefoxがシェアを伸ばしていましたが、ChromeとSafariが主流ブラウザになって現在に至ります。

今では、だいたいの処理で、文字列連結程度では、工夫する余地がほとんどなく、だいたい実用範囲内で速く動作します。
DOMやレンダリングのほうが主流のボトルネックであり、どのようなUIでデータを流していく設計にするか、のほうが重要度が高い傾向にあります。
その一つの解決方法が、無限スクロールですが、快適に使うには実装上で、さまざまな技術的課題がありますが、あまり快適なユーザー体験を提供できているサイトは残念ながら少数派の印象があります。

まあとにかく、文字列連結処理の課題は、とっくの昔に、可能な限りの最適化がほぼできていて、ボトルネックは他にありそうだ、ということのようです。

Discussion