「並列化の判断」は“どこを同時に走らせていいか”を見極めること
非同期処理のパフォーマンス最適化で一番効くのが、
「待ち時間の長い処理を、できるだけ同時に走らせる」ことです。
でも、何でもかんでも並列にすればいいわけではありません。
「これは同時にやっていい」「これは順番にやらないと壊れる」
その線引きをできるようになるのが、並列化の判断です。
ここでは、
まず「直列」と「並列」の違いを体感してから、
「どんなときに並列化してよいか」を具体的に整理していきます。
直列実行と並列実行の違いを体感する
直列実行の例(1つずつ順番に待つ)
API を2つ叩くイメージで考えます。
async function fetchUser() {
console.log("ユーザー取得開始");
await new Promise((r) => setTimeout(r, 1000)); // 1秒かかるとする
console.log("ユーザー取得完了");
return { id: 1, name: "太郎" };
}
async function fetchPosts() {
console.log("投稿取得開始");
await new Promise((r) => setTimeout(r, 1500)); // 1.5秒かかるとする
console.log("投稿取得完了");
return ["post1", "post2"];
}
async function sequential() {
const start = performance.now();
const user = await fetchUser(); // ここで1秒待つ
const posts = await fetchPosts(); // そのあと1.5秒待つ
const end = performance.now();
console.log("合計時間:", Math.round(end - start), "ms");
}
sequential();
JavaScriptこの場合、
ユーザー取得(1秒)→投稿取得(1.5秒)を順番に待つので、
合計で約 2.5 秒かかります。
「前の処理が終わるまで、次の処理を始めない」
これが直列実行です。
並列実行の例(同時に走らせて、まとめて待つ)
同じ処理を「並列」にするとこうなります。
async function parallel() {
const start = performance.now();
const userPromise = fetchUser(); // ここで開始
const postsPromise = fetchPosts(); // ここで同時に開始
const [user, posts] = await Promise.all([userPromise, postsPromise]);
const end = performance.now();
console.log("合計時間:", Math.round(end - start), "ms");
}
parallel();
JavaScriptここでは、
fetchUser → 1秒
fetchPosts → 1.5秒
が「同時に」走るので、
合計時間は「長い方の 1.5 秒」くらいになります。
ここが重要です。
「互いに依存していない非同期処理」は、
Promise.all で並列にすると“待ち時間の合計”ではなく“最大の待ち時間”で済む。
いつ「並列にしていいか」を判断する軸
1. 結果の依存関係があるかどうか
一番大事な判断軸は、
「A の結果が出ないと B が始められないか?」 です。
例えば、こういうケースは直列にするしかありません。
const user = await fetchUser(); // ユーザーIDが必要
const posts = await fetchPosts(user.id); // ユーザーIDを使って投稿取得
JavaScriptfetchPosts は user.id を必要としているので、
ユーザー取得が終わる前に投稿取得を始めることはできません。
逆に、
ユーザー情報とおすすめ商品情報のように、
互いに関係ないデータなら並列にできます。
const [user, recommended] = await Promise.all([
fetchUser(),
fetchRecommendedProducts(),
]);
JavaScriptここが重要です。
「片方の結果を、もう片方の入力に使っているか?」
使っているなら直列、使っていないなら並列候補。
2. 同じデータを“順番に更新”する必要があるか
例えば、銀行口座の残高更新のように、
「順番」が意味を持つ処理は並列にしてはいけません。
// 悪い例(イメージ)
await deposit(1000); // 入金
await withdraw(500); // 出金
JavaScriptこれを無理に並列にすると、
残高計算が壊れる可能性があります。
// 並列にしてはいけないパターン
await Promise.all([
deposit(1000),
withdraw(500),
]);
JavaScriptどちらが先に反映されるか分からないので、
結果が不定になります。
ここが重要です。
「同じリソース(同じデータ)を更新する処理」は、
基本的に直列で扱う。
並列にしていいのは“読み取りだけ”か、“互いに独立した対象”のとき。
3. 外部サービスやサーバーへの負荷
Promise.all は「全部一気に投げる」ので、
API を 100 個並列に叩く、みたいなことも簡単にできます。
const results = await Promise.all(
urls.map((url) => fetch(url))
);
JavaScriptでも、
これをそのままやると、
相手のサーバーや自分のブラウザ・Node.js に
かなりの負荷がかかることがあります。
「10件くらいなら並列でいいけど、100件は危ない」
みたいなラインが現実には存在します。
初心者の段階では、
「数が多いときは“全部並列”ではなく“何個ずつかに分ける”という発想が必要になる」
とだけ覚えておけば十分です。
4. エラーの扱い方
Promise.all は、
どれか1つでも失敗すると「全体が reject」になります。
const [a, b, c] = await Promise.all([
taskA(), // ここでエラー
taskB(),
taskC(),
]); // 全体がエラーになる
JavaScript「全部成功してほしい」処理ならこれでOKですが、
「失敗したものはスキップして、成功したものだけ使いたい」
という場合は、Promise.allSettled など別の手段を考える必要があります。
ここも判断ポイントです。
「全部成功しないと意味がない処理」なら Promise.all で並列化しやすい。
「一部だけでも使いたい」なら、並列化してもエラー処理を工夫する必要がある。
実務に近い「並列化してよい/ダメ」の例
並列化してよい典型パターン
ユーザーページで、
プロフィール・投稿一覧・通知一覧を同時に取得したいケース。
const [profile, posts, notifications] = await Promise.all([
fetchProfile(userId),
fetchPosts(userId),
fetchNotifications(userId),
]);
JavaScriptどれも「userId さえあれば独立して取得できる」ので、
並列化すると体感速度がかなり上がります。
並列化してはいけない典型パターン
「ドラフトを保存してから、そのIDを使って画像をアップロードする」ようなケース。
const draft = await saveDraft(formData); // ここで draftId が決まる
const image = await uploadImage(draft.id, file); // draft.id が必要
JavaScriptこれは依存関係があるので、
絶対に直列にする必要があります。
ここが重要です。
「人間の目線でストーリーを追ってみて、“順番が入れ替わったらおかしくなるか?”を考える。
おかしくなるなら直列、おかしくならないなら並列候補。」
初心者として「並列化の判断」で本当に押さえてほしいこと
最後に、判断の軸をシンプルにまとめます。
依存関係がないなら並列候補
結果を互いに使っていない非同期処理は、Promise.all で並列にしてよい。
同じデータを更新するなら直列
順番が意味を持つ処理(残高更新など)は、必ず順番に実行する。
数が多すぎる並列は危険
API を大量に並列で叩くと、相手も自分も苦しくなる。数を意識する。
エラーの扱いもセットで考える
全部成功前提なら Promise.all、一部成功でもよいなら別のパターンも検討。
おすすめの練習は、
自分でいくつかの非同期処理(setTimeout でもOK)を書いてみて、
「これは直列に書くべきか? 並列にしても意味が変わらないか?」
を一つずつ言語化してみることです。
コードを書くたびにその問いを挟めるようになると、
あなたは「なんとなく async/await を書く人」から、
「パフォーマンスと正しさを両方見ながら非同期処理を設計できる人」 に変わっていきます。
