JavaScript | 非同期処理:パフォーマンス最適化 - バッチ処理

JavaScript JavaScript
スポンサーリンク

「バッチ処理」は“まとめてやるから速くて優しい”という発想

バッチ処理は、
「細かい処理を1個ずつやるのではなく、ある程度まとめて一気に処理する」
という考え方です。

非同期処理のパフォーマンス最適化では、

・サーバーへのリクエスト回数を減らす
・1回あたりの処理効率を上げる
・無駄な待ち時間を減らす

といった目的で、バッチ処理がよく使われます。

イメージとしては、
「荷物を1個ずつ運ぶ」のではなく、
「台車に10個載せて一気に運ぶ」感じです。


まずは「1件ずつ処理する」素朴なコードを見てみる

1ユーザーずつ API を叩いている例

例えば、ユーザーIDの配列があって、
それぞれのユーザー情報を API から取得したいとします。

async function fetchUser(id) {
  console.log("API 呼び出し:", id);
  await new Promise((r) => setTimeout(r, 300)); // 300ms かかるとする
  return { id, name: `User ${id}` };
}

async function fetchUsersIndividually(ids) {
  const results = [];
  for (const id of ids) {
    const user = await fetchUser(id); // 1件ずつ待つ
    results.push(user);
  }
  return results;
}

fetchUsersIndividually([1, 2, 3, 4, 5]);
JavaScript

この場合、
ID が5件あるので、
300ms × 5 = 約 1500ms かかります。

しかも、サーバー側から見ると
「小さいリクエストが5回飛んでくる」状態です。

・ネットワークのオーバーヘッドが5回分
・サーバー側も5回分の処理を立ち上げる

という無駄が発生しています。


バッチ処理の発想:「まとめて1回でお願いできますか?」

サーバー側が「複数IDを一度に受け取れる」場合

もしサーバー側に、
「複数のIDを一度に渡せる API」があるなら、
話は一気に変わります。

async function fetchUsersBatch(ids) {
  console.log("バッチ API 呼び出し:", ids);
  await new Promise((r) => setTimeout(r, 400)); // まとめて 400ms かかるとする
  return ids.map((id) => ({ id, name: `User ${id}` }));
}

async function main() {
  const users = await fetchUsersBatch([1, 2, 3, 4, 5]);
  console.log(users);
}

main();
JavaScript

さっきは 1500ms かかっていた処理が、
バッチ API を使うと 400ms で終わります。

・リクエストは1回だけ
・ネットワークのオーバーヘッドも1回分
・サーバー側も「まとめて処理」できる

ここが重要です。
「同じ種類の処理を何度もやっているなら、“まとめて1回でできないか?”を考える。
それがバッチ処理の出発点。」


クライアント側で「バッチング」するというテクニック

サーバーが「1件ずつの API」しか持っていない場合

現実には、
「/user?id=1」のように、
1件ずつしか取れない API も多いです。

それでも、クライアント側で工夫して
「バッチっぽく」振る舞わせることができます。

発想はこうです。

・「今すぐ API を叩かず、少しの間“リクエスト候補”を貯めておく」
・「ある程度たまったら、まとめて処理する」

例えば、「100ms の間に来たリクエストをまとめる」ようなイメージです。

簡易的なバッチャーの例

function createBatchFetcher(fetchFn, delay = 50) {
  let queue = [];
  let timer = null;

  return function batchedFetch(arg) {
    return new Promise((resolve, reject) => {
      queue.push({ arg, resolve, reject });

      if (!timer) {
        timer = setTimeout(async () => {
          const currentQueue = queue;
          queue = [];
          timer = null;

          const args = currentQueue.map((item) => item.arg);

          try {
            const results = await fetchFn(args); // まとめて処理
            currentQueue.forEach((item, index) => {
              item.resolve(results[index]);
            });
          } catch (e) {
            currentQueue.forEach((item) => item.reject(e));
          }
        }, delay);
      }
    });
  };
}
JavaScript

この createBatchFetcher は、
「複数の引数をまとめて処理できる関数 fetchFn」を受け取り、
「1件ずつ呼べるけど、内部でバッチングしてくれる関数」を返します。

使い方のイメージはこうです。

async function fetchUsersBatch(ids) {
  console.log("バッチ API 呼び出し:", ids);
  await new Promise((r) => setTimeout(r, 300));
  return ids.map((id) => ({ id, name: `User ${id}` }));
}

const getUser = createBatchFetcher(fetchUsersBatch, 50);

async function main() {
  const p1 = getUser(1);
  const p2 = getUser(2);
  const p3 = getUser(3);

  const users = await Promise.all([p1, p2, p3]);
  console.log(users);
}

main();
JavaScript

getUser(1), getUser(2), getUser(3)
「1件ずつの関数」のように見えますが、
内部では 50ms 以内に来た呼び出しをまとめて
fetchUsersBatch([1, 2, 3]) として処理します。

ここが重要です。
「呼び出し側には“1件ずつ”のインターフェースを見せつつ、
内部では“まとめて処理”している。
これがクライアント側バッチングの気持ちよさ。」


バッチ処理のメリットとトレードオフ

メリット:回数が減る・速くなる・優しくなる

バッチ処理の主なメリットは、次の3つです。

・リクエスト回数が減る
・1回あたりの処理効率が上がる(サーバー側もまとめて処理しやすい)
・ネットワークやサーバーへの負荷が下がる

特に、
「大量の小さなリクエスト」を
「少数の大きなリクエスト」に変えられると、
体感速度もサーバーコストもかなり変わります。

トレードオフ:少し“待つ”ことになる

ただし、バッチ処理には
「少し待つ」 というトレードオフがあります。

・「すぐに1件だけ処理する」のではなく
・「他にも一緒に処理できるものが来るか、少し待ってからまとめる」

という設計にすると、
「最初の1件」は少しだけ遅くなります。

例えば、
「50ms ためてからまとめる」場合、
最初のリクエストは最大 50ms 余分に待つことになります。

ここが重要です。
「1件あたりの“最速”を目指すのか、
全体としての“効率”を上げるのか。
バッチ処理は後者を選ぶテクニック。」


非同期処理のパフォーマンス最適化としての「バッチ処理」の見方

どんなときに「バッチを考えるべきか」

こんな状況を見つけたら、バッチ処理を疑っていいサインです。

・同じ種類の非同期処理を、短時間に何度も呼んでいる
・1件あたりの処理は軽いが、回数が多くて全体が重い
・サーバー側に「まとめて処理できる API」が用意されている、または用意できそう

そのときに考えるべきことは、

・「まとめて処理したら、どれくらい速くなるか?」
・「どれくらいの時間 or 件数で“まとめる”のがちょうどいいか?」
・「少し待たされても許される UX か?」

です。

ここが重要です。
「バッチ処理は、“1回1回を最速にする”テクニックではなく、
“全体としての効率と負荷バランスを最適化する”テクニック。」


初心者として「バッチ処理」で本当に押さえてほしいこと

最後に、感覚として持っておいてほしいのはこれです。

・バッチ処理=「細かい処理をまとめて一気にやる」発想
・非同期処理では「リクエスト回数を減らす」「待ち時間をまとめる」ために使う
・サーバー側にバッチ API があるなら、積極的に使う価値が高い
・クライアント側でも「短時間に来たリクエストをまとめる」バッチングができる
・その代わり、「少し待つ」というトレードオフがある

おすすめの練習は、
自分のコードやサンプルコードの中から、

「同じ関数を短時間に何度も呼んでいる場所」

を探してみることです。

そして、自分にこう問いかけてみてください。

「これ、まとめて1回でできたら、どれくらい気持ちよくなるだろう?」

その問いを持てるようになった瞬間、
あなたはもう「コードを書く人」から一歩進んで、
「システム全体の動きと負荷を設計する人」 に近づいています。

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