ループの最適化とは何か
ループの最適化とは、「同じ結果を保ったまま、ループの中で行う仕事量を減らす」工夫のことです。1回の処理が少し重くても、ループで何千・何万回と繰り返されると全体の速度に大きく響きます。ポイントは「ループの外に出せるものは外へ」「早く終われるなら早く終わる」「より速いデータ構造を選ぶ」の3つです。
無駄な処理を減らす基本テクニック
ループ外に出せる計算は外へ
毎回同じ値になる計算は、ループの外で一度だけ計算しましょう。ループごとの重さが確実に減ります。
// 悪い例:同じ計算を毎回している
const items = [1, 2, 3, 4, 5];
for (let i = 0; i < items.length; i++) {
const taxRate = Number(process.env.TAX_RATE) || 0.1; // 毎回同じ
items[i] = items[i] * (1 + taxRate);
}
// 良い例:外に出す
const items2 = [1, 2, 3, 4, 5];
const taxRate2 = Number(process.env.TAX_RATE) || 0.1;
for (let i = 0; i < items2.length; i++) {
items2[i] = items2[i] * (1 + taxRate2);
}
JavaScript長さや定数をキャッシュする
配列の長さや不変の値を変数にとっておくと、毎回のプロパティ参照を避けられます。小さな差でも、回数が多いと効きます。
const arr = new Array(100000).fill(0);
// length をキャッシュ
for (let i = 0, len = arr.length; i < len; i++) {
arr[i] += 1;
}
JavaScript条件分岐の順序を工夫する(早期スキップ・終了)
よく起きる条件を先にチェックし、当てはまったら continue や break で早く抜けます。高頻度のケースほど前に置くと合計の仕事量が減ります。
// 早期スキップ(continue)
for (const user of users) {
if (!user.active) continue; // 非アクティブはすぐ飛ばす
if (user.role !== "member") continue;
process(user); // 条件を通過したものだけ処理
}
// 早期終了(break)
for (let i = 0; i < logs.length; i++) {
if (logs[i].level === "fatal") {
alertAdmin(logs[i]);
break; // 最初の致命的エラーで終了
}
}
JavaScriptデータ構造で高速化する
配列探索をセットで置き換える
「含まれているかどうか」を何度も判定するなら、配列より Set が高速です。配列の includes は最悪 O(n)、Set の has は平均 O(1) です。
// 配列:毎回走査する
const bannedList = ["foo", "bar", "baz"];
function isBannedByArray(x) {
return bannedList.includes(x);
}
// Set:即座に判定
const bannedSet = new Set(["foo", "bar", "baz"]);
function isBannedBySet(x) {
return bannedSet.has(x);
}
JavaScript検索を辞書(オブジェクト・Map)に
ID から素早くレコードを引くなら、Map やオブジェクトで索引を作ると速くなります。毎回配列を探すより効率的です。
// 悪い例:毎回 find(O(n))
function findByIdSlow(list, id) {
return list.find(item => item.id === id);
}
// 良い例:索引(インデックス)を事前作成(O(1))
const index = new Map();
for (const item of list) {
index.set(item.id, item);
}
function findByIdFast(id) {
return index.get(id);
}
JavaScriptループの書き方を見直す
for, for…of, forEach の選び方
パフォーマンス・可読性・必要な機能で選びます。一般にインデックスが必要なら for、要素だけで十分なら for...of がバランス良い選択です。forEach はコールバック呼び出しが増えるため、超大量データではわずかに不利になることがあります。
// インデックスが欲しい:for
for (let i = 0; i < arr.length; i++) {
/* arr[i] を使う */
}
// 要素だけで良い:for...of(読みやすい)
for (const item of arr) {
/* item を使う */
}
// 手軽だが関数呼び出しが増える:forEach
arr.forEach(item => { /* ... */ });
JavaScriptネストを減らす
二重三重のループは処理量が爆発しがちです。可能なら条件の前処理や索引化でネストを浅くし、組み合わせ数を減らします。
// 悪い例:二重ループで毎回比較
for (const a of listA) {
for (const b of listB) {
if (a.id === b.id) join(a, b);
}
}
// 良い例:索引化で一発取得
const indexB = new Map(listB.map(b => [b.id, b]));
for (const a of listA) {
const b = indexB.get(a.id);
if (b) join(a, b);
}
JavaScript入出力や副作用をまとめる
DOM 操作やログはまとめて
ループ内で DOM を何度も触る、ログを大量に出す、ネットワーク呼び出しを繰り返すのは大きな遅延の原因になります。まず計算を集約し、最後に一括で反映しましょう。
// 悪い例:毎回 DOM を更新
for (const msg of messages) {
const li = document.createElement("li");
li.textContent = msg;
list.appendChild(li); // たびたびレイアウトが発生
}
// 良い例:まとめて挿入
const frag = document.createDocumentFragment();
for (const msg of messages) {
const li = document.createElement("li");
li.textContent = msg;
frag.appendChild(li);
}
list.appendChild(frag); // 一度だけ反映
JavaScript実用例で理解する
不要な計算を外に出して高速化
// タグの正規化が高コストだと仮定
function normalize(tag) { return tag.trim().toLowerCase(); }
const posts = [/* ...多数... */];
const targetTag = normalize(" JavaScript ");
let count = 0;
// 悪い:毎回 normalize(" JavaScript ") を呼ぶ
for (const post of posts) {
if (post.tags.map(normalize).includes(targetTag)) count++;
}
// 良い:外に出す + Set で会員判定
let count2 = 0;
for (const post of posts) {
const tagSet = new Set(post.tags.map(normalize));
if (tagSet.has(targetTag)) count2++;
}
JavaScript早期終了で総コストを削減
// 最初に条件を満たすユーザーを探す
function findActiveAdmin(users) {
for (const user of users) {
if (!user.active) continue; // 非アクティブなら即スキップ
if (user.role === "admin") return user; // 見つかったら即終了
}
return null;
}
JavaScriptまとめ
ループ最適化のコツは「外に出せる計算は外へ」「よく起きる条件から早くスキップ・終了」「より速いデータ構造に置き換え」「ネストを浅く」「副作用をまとめる」の5つです。難しいテクニックより、まずこの基本を徹底するだけで体感できる改善が得られます。繰り返しが重い箇所ほど、効果は大きくなります。
