JavaScript | 配列・オブジェクト:パフォーマンス・設計 – 大量データ処理の考え方

JavaScript JavaScript
スポンサーリンク

大量データ処理とは何か

大量データ処理は「配列やオブジェクトが数万〜数百万件規模になっても、時間とメモリを破綻させずに目的を果たす」ための考え方です。ここが重要です:やみくもに map や filter を重ねると“走査回数の増加”と“中間配列の割り当て”で急速に遅くなります。計算量(何回なめるか)とメモリ(何個の配列を作るか)を常に意識し、「最小限の走査」「最小限の割り当て」に設計します。


処理の順序設計(単一パスと早期終了)

フィルタ→変換→集計→出力の順に“1回で”やり切る

大量データでは「フィルタと変換を1パスで融合し、必要なら集計を同時に行う」ことが効きます。中間配列を作らず、最後の結果のみ作るのが理想です。

function filterMapReduce(items, pred, mapFn) {
  let sum = 0;
  const out = [];
  for (const x of items) {
    if (!pred(x)) continue;         // フィルタ
    const y = mapFn(x);             // 変換
    sum += y.price ?? 0;            // 集計(例)
    out.push(y);                    // 最終出力だけ作る
  }
  return { rows: out, sum };
}
JavaScript

ここが重要です:filter→map→reduce の三段チェーンを1回の for…of にまとめると、走査は1回・割り当ても1回になり、GCプレッシャーが大幅に下がります。

早期終了を徹底する

必要なものが見つかったら終わる設計にします。find、some、every は“途中で止まれる”ため、全件走査の無駄を省けます。

const hit = items.find(x => x.id === targetId);  // 見つかれば即終了
const hasExpensive = items.some(x => x.price > 100000);
JavaScript

計算量を落とす定番手法(辞書化・前計算・ソート回避)

重複検索は辞書化に切り替える

同じキー(ID)で何度も探すなら、配列から辞書(オブジェクト/Map)へ変換し、検索を O(1) にします。

const dict = Object.fromEntries(items.map(x => [String(x.id), x]));
const pick = ids => ids.map(id => dict[String(id)]).filter(Boolean);
JavaScript

ここが重要です:二重ループや毎回 find で O(n*k) になりがちな処理を、辞書で O(n+k) に変えます。

比較用のキーを前計算する(Schwartzian transform)

重い正規化や日付変換は“比較・判定の前”に1回だけ行い、以後は前計算キーで高速に処理します。

const prepared = items.map(x => ({
  x,
  keyName: x.name.trim().toLowerCase(),
  keyDate: new Date(x.createdAt).getTime()
}));

const recent = [];
for (const p of prepared) {
  if (p.keyName.includes("alice") && p.keyDate > cutoffMs) recent.push(p.x);
}
JavaScript

ここが重要です:new Date や正規化を何十万回も繰り返すと定数倍で激遅になります。前計算で“比較を軽くする”のが王道です。

ソートは本当に必要かを見直す

sort は O(n log n) で高コストです。トップNが欲しいだけなら“部分選抜”でソートを回避できます。

function topN(items, n) {
  const heap = [];                 // 簡易:最小値を先頭に保持
  for (const x of items) {
    heap.push(x);
    heap.sort((a, b) => a.score - b.score); // 小さい順
    if (heap.length > n) heap.shift();      // 最小を捨てる
  }
  return heap.toSorted((a, b) => b.score - a.score); // 見やすく整列
}
JavaScript

ここが重要です:全体ソートを避け、必要部分だけ維持する発想にすると、n が小さいほど劇的に速くなります。


メモリ戦略(チャンク処理・ストリーミング・中間削減)

チャンク処理でピークメモリを抑える

巨大配列は一定サイズで分割して順次処理します。部分結果をその都度出力・集約すれば、同時に抱えるメモリ量を減らせます。

function processInChunks(arr, size = 10000, handle) {
  for (let i = 0; i < arr.length; i += size) {
    const chunk = arr.slice(i, i + size);
    handle(chunk);                 // ここで集計や書き出し
  }
}
JavaScript

ここが重要です:一度に全てをメモリに載せず、段階的に処理していく。スループットと安定性が向上します。

中間配列を作らない(融合する)

filter と map をチェーンすると“中間配列”が増えます。1パスで結果だけ作る関数を用意して、割り当て数を減らします。

function filterMap(arr, pred, mapFn) {
  const out = [];
  for (const x of arr) if (pred(x)) out.push(mapFn(x));
  return out;
}
JavaScript

ここが重要です:大量データでは“新しい配列を作る回数”がボトルネックになります。最後の出力だけ作ると GC が安定します。

破壊と非破壊の使い分け

外部に影響しないローカル一時データは“破壊的操作”で割り当てを減らし、共有状態やUIは“非破壊”で安全性を保つ方針が現実的です。

const tmp = getChunk();     // 新規生成なら in-place が安全
tmp.sort((a,b)=>a-b);       // 破壊的でも外へ漏れない前提
consume(tmp);
JavaScript

I/O と並列の工夫(API・ファイル・UI)

ページング・カーソルで“必要な分だけ”取得する

外部データは“全部ください”を避け、ページングまたはカーソルで必要な範囲だけ取る。取得前にサーバー側でフィルタ・ソートを済ませてもらうと最も効率的です。

function cursorPage(list, cursorId = null, limit = 2000) {
  const i = cursorId == null ? -1 : list.findIndex(x => x.id === cursorId);
  const start = Math.max(0, i + 1);
  const rows = list.slice(start, start + limit);
  return { rows, nextCursor: rows.at(-1)?.id ?? null };
}
JavaScript

ここが重要です:データは“持ちすぎない”。I/Oの段階で絞ることが、メモリと時間の最大の節約になります。

UI は“仮想化”でレンダリングを絞る

画面表示は全件を描画せず、見えている範囲だけ表示(Virtualization)。配列処理自体より、レンダリングコストがボトルネックになることが多いです。配列はページング・検索結果だけ渡す設計にします。


失敗しないためのガードと計測

まず計測する(想像で最適化しない)

実データ規模で console.time や軽いベンチを取り、どこが遅いかを把握してから手を入れます。

console.time("pipeline");
const out = filterMapReduce(items, pred, mapFn);
console.timeEnd("pipeline");
JavaScript

ここが重要です:小さな配列では差が見えません。“大きいほど効く最適化”に注力するため、計測をルーチン化します。

ガードで安全化(欠損・型揺れを潰す)

大量データは欠損が混ざりがちです。null/undefined を先に除外・既定値化して、コールバックの分岐を軽くします。

const safeItems = (items ?? []).filter(Boolean);
JavaScript

すぐ使えるレシピ(大量データでも耐える定番)

一括フィルタ・変換・集計(単一パス)

function pipeline(items, pred, mapFn) {
  const out = [];
  let count = 0, sum = 0;
  for (const x of items) {
    if (!pred(x)) continue;
    const y = mapFn(x);
    out.push(y);
    count++;
    sum += y.price ?? 0;
  }
  return { rows: out, meta: { count, sum } };
}
JavaScript

チャンク+逐次出力

function processLarge(items, handle, size = 10000) {
  for (let i = 0; i < items.length; i += size) {
    const chunk = items.slice(i, i + size);
    handle(chunk); // 書き出し・集約・送信など
  }
}
JavaScript

辞書化+部分復元

const toDict = (list, key = "id") =>
  Object.fromEntries(list.map(x => [String(x[key]), x]));

function updateDict(dict, id, patch) {
  return { ...dict, [id]: { ...dict[id], ...patch } };
}
function toList(dict) { return Object.values(dict); }
JavaScript

まとめ

大量データ処理の核心は「単一パス化で走査回数と中間配列を減らす」「辞書化・前計算で計算量を落とす」「チャンク処理・ページングで“必要な分だけ持つ”」ことです。ソートは本当に必要なときだけ、トップNは部分選抜で代替。ローカルは破壊的で割り当てを減らし、共有は非破壊で安全に。計測を習慣化し、欠損ガードでコールバックを軽く保つ。この指針に従えば、初心者でもスケールしても壊れない、速くて安定した大量データ処理を設計できます。

タイトルとURLをコピーしました