まず「並列処理」をざっくりイメージでつかむ
JavaScript の Promise でいう「並列処理」は、
「待ち時間がある処理を “同時にスタート” させて、全部終わる(またはどれかが終わる)のを待つやり方」
だと思ってください。
よくある勘違いとして、
「JavaScript が CPU を何コアも使って、ガンガン同時に計算している」
みたいに思われがちですが、そこは違います。
Promise で言う「並列」は、
サーバーへのリクエスト
ファイル読み込み(ブラウザなら IndexedDB など)
タイマー待ち
といった「外部の処理」を、複数いっぺんに動かして“待ち時間をかぶせる” ことに意味があります。
ここが重要です。
Promise の並列処理は「計算を同時にやる」というより、「複数の“待ち”を同時に走らせて、全体の時間を短くする」ための考え方 です。
直列処理と並列処理の違い(時間のイメージ)
直列処理のイメージ
まず、「順番に実行する」直列処理から考えます。
たとえば、3 つの非同期処理があるとします。
A:1 秒かかる
B:2 秒かかる
C:1.5 秒かかる
これを「順番に」やるときの典型的な書き方はこうです。
doA() // 1秒かかる Promise
.then((resultA) => {
return doB(resultA); // さらに 2秒
})
.then((resultB) => {
return doC(resultB); // さらに 1.5秒
})
.then((resultC) => {
console.log("全部完了:", resultC);
});
JavaScript時間の合計としては、おおよそ
1 秒(A)
+ 2 秒(B)
+ 1.5 秒(C)
= 4.5 秒
くらいかかるイメージです。
「A が終わってから B」「B が終わってから C」と、完全に順番待ち をしています。
並列処理のイメージ
同じ 3 つの処理を、「互いに依存していないなら」まとめてスタートしてしまうのが並列処理です。
const pA = doA(); // すぐにスタート
const pB = doB(); // すぐにスタート
const pC = doC(); // すぐにスタート
Promise.all([pA, pB, pC]).then(([resultA, resultB, resultC]) => {
console.log("全部完了:", resultA, resultB, resultC);
});
JavaScript3 つとも 同時に走り始める ので、
待ち時間は「一番遅いもの」にほぼ等しくなります。
A:1 秒
B:2 秒
C:1.5 秒
なら、全体の時間はざっくり 2 秒 です。
ここが重要です。
直列処理:時間 = A + B + C
並列処理:時間 ≒ max(A, B, C)
この差が「Promise で並列処理を考える価値」です。
JavaScript は「マルチスレッド」じゃないのに、なぜ並列っぽくできるのか
シングルスレッド+非同期の組み合わせ
JavaScript(ブラウザのメインスレッド)は基本的にシングルスレッドです。
「同時に 2 行のコードを実行する」ことはしていません。
じゃあなんで「並列っぽい」ことができるのかというと、
非同期処理(ネットワーク、タイマー、ファイルなど)は JavaScript の外側の仕組みが担当してくれる
→ JavaScript 側は「終わったら呼んでね」とだけ登録して、いったん手を離す
→ 終わったらイベントループを通じてコールバック / then が呼ばれる
という流れがあるからです。
Promise にすると、これを「上から下に読みやすく」書けるわけですね。
並列処理は「CPU を同時に動かす」ことではない
ここでの本質は、
ネットワーク待ち
ディスク待ち
タイマー待ち
といった「待ち時間」は CPU をあまり使っていないので、
そのあいだに他の待ちを並べてしまおう という発想です。
だからこそ、
重い for ループを Promise で包んでも速くはならない
→ 計算そのものは JavaScript スレッドで順番に行われるから
一方で、
API 呼び出しを Promise.all で並列にすると速くなる
→ 待ち時間を重ねられるから
という違いが出ます。
ここが重要です。
Promise の「並列処理」は、「待ち時間をうまく重ね合わせるテクニック」であって、「JS がマルチコアで計算を分散している」わけではない、と理解しておくと誤解が減ります。
Promise.all を使った並列処理の基本形
同時にスタートさせる書き方
並列処理のいちばん基本的なパターンは、Promise.all です。
例として、
ユーザー情報
投稿一覧
通知一覧
を「同時に」取得して、揃ってから画面に表示したいケースを考えてみます。
function fetchUser() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("ユーザー取得完了");
resolve({ id: 1, name: "Taro" });
}, 1000);
});
}
function fetchPosts() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("投稿取得完了");
resolve(["投稿1", "投稿2"]);
}, 1500);
});
}
function fetchNotifications() {
return new Promise((resolve) => {
setTimeout(() => {
console.log("通知取得完了");
resolve(["通知A", "通知B"]);
}, 500);
});
}
JavaScript直列版と並列版を比べてみます。
悪い例:直列に書いてしまう
// 直列(順番に待っているだけ)
fetchUser()
.then((user) => {
return fetchPosts().then((posts) => [user, posts]);
})
.then(([user, posts]) => {
return fetchNotifications().then((notifications) => [
user,
posts,
notifications,
]);
})
.then(([user, posts, notifications]) => {
console.log("全部:", user, posts, notifications);
});
JavaScript時間はざっくり、
ユーザー 1 秒
→ 投稿 1.5 秒
→ 通知 0.5 秒
合計で約 3 秒かかります。
良い例:並列で全部投げてから待つ
// 並列(全部同時にスタート)
Promise.all([fetchUser(), fetchPosts(), fetchNotifications()])
.then(([user, posts, notifications]) => {
console.log("全部:", user, posts, notifications);
})
.catch((err) => {
console.error("どれかでエラー:", err);
});
JavaScriptこの場合の時間は、
ユーザー:1 秒
投稿:1.5 秒
通知:0.5 秒
なので、約 1.5 秒(=一番遅い投稿)で済みます。
ここが重要です。
「互いに依存していない非同期処理」は、なるべく早めに全部スタートさせて、Promise.all などでまとめて待つ。
これが Promise での並列処理の基本パターン です。
「並列にしていい処理」と「直列にすべき処理」を見分ける
並列にしていいのは「互いに依存しない処理」
次のようなケースは、並列にして問題ありません。
ユーザー情報の取得と、ランキング情報の取得(互いに関係ない)
複数の画像ファイルのダウンロード(どれも独立)
ユーザー一覧と、その日の天気情報(関係ない)
つまり、「A の結果が B の入力になっていない」処理同士 は並列候補です。
直列にすべきなのは「前の結果を使う処理」
逆に、次のようなケースは直列にすべきです。
ユーザー ID を取ってから、そのユーザーの投稿を取る
投稿 ID を取ってから、その投稿のコメントを取る
こういう場合、
「先に A が終わって ID がわからないと、B を呼びようがない」 ので、
無理に Promise.all で並列にすることはできません。
例:
fetchUser()
.then((user) => fetchPosts(user.id))
.then((posts) => fetchComments(posts[0].id))
.then((comments) => {
console.log(comments);
});
JavaScriptこれはきちんと「直列」である必要があります。
ここが重要です。
並列にできるのは「独立した非同期処理」だけ。
依存関係のある処理は、素直に直列で書くのが正しい。
そのうえで、「独立しているところは全部まとめて投げる」というスタンスを持つと、設計がきれいになります。
エラーと並列処理の関係(Promise.all / allSettled)
Promise.all は「1つでも失敗したら全体エラー」
並列処理でよく使う Promise.all は、
全部成功 → 成功として results を返す
1つでも失敗 → その時点で全体をエラーにする
という「オールオアナッシング」な動きです。
Promise.all([p1, p2, p3])
.then(([r1, r2, r3]) => {
// 3つとも成功した場合
})
.catch((err) => {
// どれか 1つでも失敗した場合
});
JavaScript「どれか 1 つでも失敗したら画面をエラーにしたい」ようなケースなら、これで OK です。
部分的な成功も扱いたいなら Promise.allSettled
一方で、
3 つのうち 2 つだけ成功していても、その 2 つを使いたい
失敗したものは失敗したものでログしたい
といった、「全部成功しなくてもいいから、全員の結果を知りたい」 ケースでは Promise.allSettled のほうが向いています。
Promise.allSettled([p1, p2, p3]).then((results) => {
results.forEach((r) => {
if (r.status === "fulfilled") {
console.log("成功:", r.value);
} else {
console.error("失敗:", r.reason);
}
});
});
JavaScriptここが重要です。
「全部成功してほしい並列処理」なら all、
「成功と失敗を混ぜて扱いたい並列処理」なら allSettled。
並列処理の設計では、「失敗したときにどう扱うか」を一緒に考えるのがセットです。
並列処理の“感覚”を身につけるための考え方
「時間軸」で考えるクセをつける
Promise で並列処理を設計するときは、コードだけ見るのではなく、
この処理はだいたい何秒くらいかかるか
これは本当に前の結果を待つ必要があるのか
待ち時間を重ね合わせられないか
という「時間軸」で考えてみると、グッと理解が深まります。
例えば、3 つの API 呼び出しがあるとします。
ユーザー情報:1 秒
ニュース一覧:2 秒
天気情報:1 秒
これを直列にすると 4 秒ですが、
並列にすると 2 秒で済みます。
しかも、コードはこう書けます。
Promise.all([fetchUser(), fetchNews(), fetchWeather()])
.then(([user, news, weather]) => {
// ここで全部使える
});
JavaScriptこの「時間削減のイメージ」を持てると、
「どこを並列化すべきか」を自然に考えられるようになります。
「とりあえず全部直列」から、「ここは並列にしてみよう」へ
初心者のうちは、
まずは素直に直列で書いてみる
→ 実行時間や処理の流れを確認する
→ 依存関係のない部分を見つけて、Promise.all にまとめる
という二段階アプローチがおすすめです。
いきなり全部を並列で書こうとすると、
「どこが何を待っているのか」が分かりにくくなりがちです。
まずは「正しく動く直列版」を作る。
そのうえで、「ここは独立しているから同時に投げていいな」と分かった箇所を並列化していく。
ここが重要です。
並列処理は「正しさ+速さ」の両方を意識しなければならないので、最初は“正しさ優先で直列”、慣れてきたら“速さを意識して並列”という順番で攻めると安全です。
まとめ:Promise での並列処理の考え方
最後に、Promise の並列処理をシンプルにまとめます。
JavaScript の「並列処理」は、CPU を増やす話ではなく、「複数の“待ち”を同時に走らせて、全体の時間を短くする」発想。
互いに依存していない非同期処理(API 呼び出しなど)は、なるべく早く全部スタートさせて、Promise.all や allSettled でまとめて待つと効率がいい。
直列処理:時間 ≒ A + B + C
並列処理:時間 ≒ max(A, B, C)
という違いを「時間のイメージ」として持っておく。
依存関係のある処理(ユーザー ID → 投稿 → コメントなど)は、素直に直列で書くべき。
無理に並列にしようとするのではなく、「どこまで直列で、どこから並列か」を見極める。
Promise.all は「全部成功前提」、Promise.allSettled は「成功と失敗を両方レポート」、用途を分ける。
まずは直列で正しく書き、そこから「待ち時間を重ねられる部分」を見つけて並列化する、というステップで練習すると身につきやすい。
もしよければ、
「3つの setTimeout(1秒、1.5秒、2秒)を直列で待つバージョン」と
「Promise.all で並列に待つバージョン」
を自分で書いて、実際にかかる時間を console.log で比べてみてください。
その体験をすると、
「並列処理を考える価値(待ち時間を重ねることの意味)」 がかなりリアルに分かってくるはずです。
