map / filter のコストとは何か
map / filter は「要素ごとにコールバックを呼び、結果配列を新しく作る」処理です。ここが重要です:計算量は基本的に (O(n)) ですが、時間とメモリの“定数コスト”が無視できません。具体的には「コールバック呼び出しのオーバーヘッド」「新配列の割り当て」「要素コピー(filter)」が累積します。
// 典型例:二段チェーン(2回走査+2回割り当て)
const out = arr
.filter(x => x.active) // 要素を間引き、新しい配列Aを作る
.map(x => x.value * 2); // さらに全要素に関数適用、新しい配列Bを作る
JavaScript時間コストの内訳(1要素あたりのオーバーヘッド)
コールバック呼び出しのコスト
- 関数呼び出し: 1要素ごとに関数境界を跨ぐため、単純な
forよりわずかに重くなります。 - クロージャのキャプチャ: 外部変数を参照すると最適化しづらくなることがあり、定数をローカルへ“ホイスト”すると軽くなります。
const factor = 2;
const out = arr.map(x => x * factor); // factor は外側よりローカルに近い方が速いことが多い
JavaScript走査回数のコスト
- 単段: (O(n)) の1回走査。
- チェーン: filter→map のように二段にすると、走査が2回になり (O(2n))(定数倍の増加)になります。大量データで差が出ます。
メモリ/割り当てのコスト(GC とプレッシャー)
新配列の割り当てとコピー
- filter: 条件を満たす要素を新配列へ“選別コピー”。選別比率が高いほどコピー量が増えます。
- map: 常に同サイズの新配列を作成。元配列は残るためメモリピークが上がります。
const filtered = arr.filter(pred); // 新配列A(サイズは通過要素数)
const mapped = filtered.map(fn); // 新配列B(サイズはAと同じ)
JavaScriptガベージコレクション(GC)への影響
- 短命の中間配列: チェーン途中の一時配列がすぐ破棄され、GCが頻発。大きい配列連鎖では遅延の原因になります。
- 対策: “単一パスで最終出力だけ”を作ると中間配列を減らせます。
チェーンを1回の走査にまとめる(フュージョン)
for…of で一括処理(最も簡潔で速い)
const out = [];
for (const x of arr) {
if (!x.active) continue; // filter 相当
out.push(x.value * 2); // map 相当
}
JavaScriptここが重要です: filter と map を同時にやると、走査は1回・割り当ても1回。定数倍のコストを抑えられ、GCプレッシャーも減ります。
reduce で合成(関数型に寄せたいとき)
const out = arr.reduce((acc, x) => {
if (x.active) acc.push(x.value * 2);
return acc;
}, []);
JavaScriptここが重要です: reduce は“累積器へ積む”ことで filter+map を融合できます。チームのコーディングスタイルに合わせて選びます。
早期終了が必要なときの代替(map/filterは最後まで回る)
find / some / every の採用
- find: 条件に合致した最初の1件を返す(見つかったら終了)。
- some: 条件に合致する要素が存在するか(trueで終了)。
- every: すべてが条件を満たすか(falseで終了)。
const hit = arr.find(x => x.id === targetId); // 早期終了で無駄を省く
const hasActive = arr.some(x => x.active);
JavaScriptここが重要です: map / filter は最後まで走査します。1件が見つかれば十分な場面では早期終了系を選ぶと大幅に速くなります。
実務で効く最適化パターン
キーの前計算で比較を軽く(Schwartzian transform)
const prepared = arr.map(x => ({
x,
nameKey: x.name.trim().toLowerCase()
}));
// 事前計算したキーだけで判定・変換
const out = prepared
.filter(p => p.nameKey.includes("alice"))
.map(p => p.x); // 中間配列は1回だけ(用途次第でfor...of融合も可)
JavaScriptポイント: 重い処理(trim, toLowerCase, new Date)をコールバック内で毎回行わず、前計算キーを使う。
flatMap で “map→flat” を融合
const out = arr.flatMap(x => (x.items ?? []).map(it => it.value));
JavaScriptポイント: ネスト配列を展開しつつ変換するなら、flatMap で走査と割り当てを減らせます(内部は1パス)。
条件分岐を外へ(述語を固定化)
const pred = x => x.active && x.score >= 80;
const out = arr.filter(pred).map(x => x.name);
JavaScriptポイント: 毎回条件を組み立てるより、述語関数を外出しして最適化・テストしやすくする。
map / filter を使うべき場面と避けるべき場面
使うべき場面(可読性が勝つ)
- 小〜中規模配列: 数千件程度までなら“意図が明確で短い”利点が大きい。
- 純粋な変換/選別: 関数合成の見通しが良く、レビューしやすい。
避けるべき場面(性能が気になる)
- 超大規模(数十万件〜)や高頻度反復: 単一パスへ融合(for…of/reduce)して中間配列を作らない。
- 早期終了が有効: find / some / every を使う。
計測と防御(ベンチの取り方とガード)
まず測る(計測なしで最適化しない)
console.time("pipeline");
const out = arr.filter(pred).map(fn);
console.timeEnd("pipeline");
JavaScriptポイント: 実データ規模で測る。小さなテストだと差が出ないことが多い。
ガードと既定値で安定化
const list = data ?? []; // 欠損でも安全
const out = list.filter(Boolean); // null/undefined を落とす簡易フィルタ
JavaScriptポイント: 入力が欠損しても落ちないように受け皿を用意する。無駄な分岐を減らしてコールバックを軽く保つ。
すぐ使えるレシピ(チェーン削減と一括処理)
filter+map を1パスで
function filterMap(arr, pred, mapFn) {
const out = [];
for (const x of arr) {
if (!pred(x)) continue;
out.push(mapFn(x));
}
return out;
}
// 例
const out = filterMap(arr, x => x.active, x => x.value * 2);
JavaScript重い変換の前計算
const prepared = arr.map(x => ({ x, d: new Date(x.createdAt).getTime() }));
const recent = [];
for (const p of prepared) {
if (p.d > cutoffMs) recent.push(p.x);
}
JavaScript早期終了の置き換え
const exists = arr.some(x => x.status === "active"); // map/filter の代わりに
JavaScriptまとめ
map / filter のコストは「1要素ごとの関数呼び出し」「中間配列の割り当て」「走査回数の増加」が核です。大量データや頻度が高い箇所では、filter+map を1パスに融合し、早期終了系(find/some/every)を選び、重い処理は前計算キーに寄せる。小〜中規模では可読性の利点が勝るため map / filter を素直に使い、必要なときだけ実測に基づいて最適化する。これを徹底すれば、初心者でも読みやすさと速度の両立ができるコードを書けます。
