配列から文字列を作る際のパフォーマンス比較

オブジェクトの配列を利用して HTML 文字列を組み立てて innerHTML で挿入、みたいなことを時々やる。昔ながらの平易な書き方をすると、こんな感じのコード。

// 何らかオブジェクトの配列がある
const items = [
  { name: '商品名 1', price: 100 },
  { name: '商品名 2', price: 500 },
  ...
];

// HTML 文字列を組み立てる : ココではリストだが、テーブルを作ったりだとか…
let html = '<ul>';
for(let i = 0; i < items.length; i++) {
  const item = items[i];
  html += '<li>' + item.name + ' … ' + item.price + '円</li>';
}
html += '</ul>';

// 組み立て終わったら挿入する、みたいなコード
document.body.innerHTML = html;

最近はこんな風に純粋な for ループを描いたり、let html+= で代入、みたいなことはあんまりやらなくなってきたものの、自分の中で2種類のやり方があるなーと思った。

一つは Array.reduce() を使って、上述のループ処理に似たような書き方をする方法だ。

もう一つは、Array.map() で要素ごとに HTML を組み立て、最後に Array.join() で文字列になるように結合する、という方法。

この2つのどちらの手法の方が、パフォーマンス的に高速なのか、気になったので調べてみた。

テストコード

次のようなコードを作って、Node.js やブラウザで実行して実行速度を確認してみた。

// Node.js の場合は performance.now() を使うためにこの require() が必要
const { performance } = require('perf_hooks');

// 指定の要素数の配列を作る関数
const createArray = loop => [...Array(loop).keys()];

// Array.reduce() で HTML を組み立てる関数
const reduce = array => {
  const start = performance.now();
  const result = array.reduce((html, i) => `${html}<div>${i}</div>\n`, '');
  const end = performance.now();
  console.log('Reduce', { start, end, time: end - start });
  return result;
};

// Array.map() と Array.join() で HTML を組み立てる関数
const mapJoin = array => {
  const start = performance.now();
  const result = array.map(i => `<div>${i}</div>\n`).join('');
  const end = performance.now();
  console.log('Map Join', { start, end, time: end - start });
  return result;
};

// 負荷をかけるためループ回数を大きめに指定する
const loop = 10_000_000;
const array = createArray(loop);

// 実際はいずれかの関数を交互に、時間を置いて複数回実行している
reduce(array);
mapJoin(array);

テキトーな配列を作って、<div> で囲んで結合する、みたいな感じの簡単なコード。それぞれの関数を、時間を開けて複数回実行してみて、大体の平均値を確認した。

実行結果は…

手元の環境で確認したところ、次のようになった。

ループ回数 … 実行時間  (ミリ秒)
 1_000_000 …  422.0333820581436
 5_000_000 … 2092.5528020858765
10_000_000 … 4118.7338089942930

100万回のループで 400 ミリ秒程度。あとは大体コレの掛け算という感じだった。

ループ回数 … 実行時間  (ミリ秒)
 1_000_000 …  274.9353690147400
 5_000_000 … 1233.8456059694290
10_000_000 … 2640.0585910081863

さて、コチラは100万回のループの時点から大きな差が見られる。100万回で 270 ミリ秒程度と、2倍まではいかないが1.5倍近い速さで完了している。

…というワケで、やる前からなんとなく予想はついていたが、Array.map()Array.join() を使う方法の方が、Array.reduce() を使う方法よりも1.5倍近く高速である、という結果になった。

「予想はついていた」というのは、Array.reduce() 内部に出来る一時変数に無駄があるからである。Array.reduce() は直前の処理結果を、内部の仮引数 html に蓄えており、コレに都度都度文字列結合していたので、そこの処理が余計だったと思われる。Array.reduce() は本来、それぞれの計算結果を返していくモノなので、このような「文字列結合」に利用するのは、「仕様的に可能」ではあっても、関数の元々の意味的にはあまりそぐわないように思う。

一方、Array.map() で下処理をして、その配列をただ Array.join('') で結合して文字列に変換するだけなら、ネイティブな部分に文字列結合処理を任せられるし、ループのたびに一時変数が出来るようなこともないので高速だったのだろう。それぞれの関数の意味合い的にも、「配列の形を変換する」「配列を結合して文字列にする」ということで読み取りやすい。

今まで、つい Array.reduce() を使っている場面があったのだが、パフォーマンス的にも、コードの可読性的にも、関数の元々の意味的にも、Array.map() + Array.join() を使う方が良いことが分かった。