JavaScriptの待機処理を理解しよう!タイミング制御の基本から実践まで
こんにちは、とまだです。
「この処理をもう少し遅らせたい」「APIの結果を待ってから次の処理を実行したい」という場面に遭遇したことはありませんか?
今回は、JavaScriptで処理のタイミングを制御する待機処理について解説します。
待機処理はなぜ必要なのか
Webアプリケーションを作っていると、すべての処理を即座に実行すれば良いわけではありません。
例えば、フォームを送信した直後に「送信完了!」と表示されても、ユーザーは本当に送信されたのか不安になることがあります。少し間を置いてから表示することで、「処理が行われた」という実感を与えられます。
また、外部のAPIを呼び出す場合、レスポンスが返ってくるまでには時間がかかります。この間、ユーザーに何も見せないと「フリーズした?」と思われてしまいます。適切なローディング表示と組み合わせて、待機処理を実装することが重要です。
待機処理が必要になる主な場面は次のとおりです。
- ユーザー体験を向上させるための演出
- 外部リソース(API、画像など)の読み込み待ち
- アニメーション間のタイミング調整
- サーバー負荷を考慮した処理の間隔調整
- エラー時のリトライ処理
これらの場面で適切な待機処理を実装することで、スムーズで快適なWebアプリケーションを作ることができます。
JavaScriptの非同期処理の仕組み
待機処理を理解するには、まずJavaScriptの非同期処理の仕組みを知る必要があります。
JavaScriptはシングルスレッドで動作します。つまり、一度に実行できる処理は1つだけです。しかし、Webアプリケーションでは複数の処理を同時に扱う必要があります。
そこで登場するのが「イベントループ」という仕組みです。
イベントループは、実行待ちの処理をキューに入れて順番に処理します。非同期処理は一旦キューに入れられ、メインの処理が終わってから実行されます。これにより、時間のかかる処理があってもブラウザがフリーズすることなく、他の処理を続けられます。
この仕組みを理解していないと、「なぜsetTimeoutで0ミリ秒を指定しても、すぐに実行されないのか」といった疑問にぶつかることになります。
基本的なsetTimeoutの使い方と注意点
最も基本的な待機処理はsetTimeout
関数です。指定した時間(ミリ秒)後に処理を実行します。
console.log('処理開始');
setTimeout(() => {
console.log('2秒後に実行されます');
}, 2000);
console.log('この行はすぐに実行されます');
このコードを実行すると、「処理開始」「この行はすぐに実行されます」と表示された後、2秒後に「2秒後に実行されます」と表示されます。
setTimeoutは非同期で動作するため、タイマーをセットした後の処理はすぐに実行されます。これは料理で例えると、タイマーをセットしてから他の作業を進めるのと同じです。
setTimeoutの落とし穴
ただし、setTimeoutには注意すべき点があります。
まず、指定した時間は「最短でその時間後」という意味です。他の処理が実行中の場合、それが終わるまで待たされることがあります。正確なタイミングが必要な場合は、別の方法を検討する必要があります。
また、setTimeoutは時間ベースの待機なので、処理の完了を保証しません。例えば、「APIの処理に2秒かかるから3秒待てば大丈夫」という考えは危険です。ネットワークの状況によっては5秒かかるかもしれませんし、エラーが発生するかもしれません。
Promiseで処理の完了を待つ
より確実に処理の完了を待つには、Promiseを使います。
Promiseは「約束」という意味で、将来的に値が返ってくることを約束するオブジェクトです。処理が成功すれば約束が果たされ(resolve)、失敗すれば破棄されます(reject)。
汎用的な待機関数を作ってみましょう。
function wait(ms) {
return new Promise(resolve => {
setTimeout(resolve, ms);
});
}
// 使い方
wait(1000)
.then(() => {
console.log('1秒経過しました');
return wait(2000);
})
.then(() => {
console.log('さらに2秒経過しました');
});
このwait
関数は、指定した時間後にPromiseを解決します。Promiseを使うことで、複数の非同期処理を連鎖させることができます。
Promiseの大きな利点は、エラーハンドリングが統一的に行えることです。.catch()
メソッドを使えば、チェーン内のどこでエラーが発生しても捕捉できます。
async/awaitで同期的に書く
Promiseをより直感的に扱えるのが、async/awaitです。
async/awaitを使うと、非同期処理をあたかも同期処理のように書けます。これにより、コードの可読性が大幅に向上します。
async function processWithDelay() {
console.log('処理を開始します');
// 1秒待つ
await wait(1000);
console.log('1秒経過しました');
// さらに2秒待つ
await wait(2000);
console.log('合計3秒経過しました');
console.log('すべての処理が完了しました');
}
// 実行
processWithDelay();
awaitキーワードは、Promiseが解決されるまでその行で処理を一時停止します。ただし、関数全体がブロックされるわけではなく、他の処理は並行して実行できます。
async/awaitの素晴らしい点は、try/catchを使った自然なエラーハンドリングができることです。同期処理と同じように例外処理を書けるため、理解しやすく保守しやすいコードになります。
実践的な使用例:API呼び出しとリトライ処理
実際の開発でよく遭遇するのが、API呼び出しでの待機処理です。
外部APIを呼び出す場合、ネットワークエラーや一時的なサーバーエラーが発生することがあります。そのため、エラー時に自動的にリトライする仕組みを実装することが重要です。
以下は、エラー時に最大3回までリトライする関数の例です。
async function fetchWithRetry(url, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
return await response.json();
} catch (error) {
console.log(`試行 ${attempt}/${maxRetries} 失敗:`, error.message);
// 最後の試行でなければ待機してリトライ
if (attempt < maxRetries) {
// 指数バックオフ:試行回数が増えるごとに待機時間を延ばす
const waitTime = Math.pow(2, attempt) * 1000;
console.log(`${waitTime}ミリ秒後にリトライします...`);
await wait(waitTime);
} else {
// すべての試行が失敗した場合
throw new Error(`${maxRetries}回の試行すべてが失敗しました`);
}
}
}
}
この実装では、「指数バックオフ」という戦略を使っています。1回目の失敗後は2秒、2回目は4秒、3回目は8秒待つという具合に、待機時間を徐々に延ばしています。
これにより、一時的な問題であればすぐに復旧する可能性が高く、継続的な問題であればサーバーに過度な負荷をかけずに済みます。
複数のAPIを効率的に呼び出す
複数のAPIを呼び出す場合、順番に実行すると時間がかかります。独立したAPIであれば、並列で実行する方が効率的です。
// 非効率な例:順番に実行
async function fetchSequential() {
const user = await fetch('/api/user').then(r => r.json());
const posts = await fetch('/api/posts').then(r => r.json());
const comments = await fetch('/api/comments').then(r => r.json());
return { user, posts, comments };
}
// 効率的な例:並列実行
async function fetchParallel() {
const [user, posts, comments] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { user, posts, comments };
}
Promise.allを使うと、すべてのPromiseが解決されるまで待機します。3つのAPIがそれぞれ1秒かかる場合、順番に実行すると3秒かかりますが、並列実行なら1秒で完了します。
ただし、Promise.allは1つでも失敗するとすべて失敗扱いになります。一部の失敗を許容したい場合は、Promise.allSettledを使います。
パフォーマンスとユーザー体験の考慮
待機処理を実装する際は、パフォーマンスとユーザー体験のバランスを考える必要があります。
長時間の処理でUIがフリーズすると、ユーザーは不快に感じます。重い処理を実行する場合は、定期的に制御をブラウザに返すことが重要です。
例えば、大量のデータを処理する場合、小分けにして処理することでUIの応答性を保てます。
async function processLargeData(data) {
const chunkSize = 100;
const totalItems = data.length;
for (let i = 0; i < totalItems; i += chunkSize) {
// 一部分だけ処理
const chunk = data.slice(i, i + chunkSize);
processChunk(chunk);
// 進捗を表示
const progress = Math.round((i / totalItems) * 100);
updateProgress(progress);
// UIに制御を返す(0ミリ秒でもイベントループが回る)
await wait(0);
}
}
また、ユーザーに待機時間を感じさせない工夫も大切です。スケルトンスクリーンやプログレスバーを表示することで、「処理が進んでいる」ことを視覚的に伝えられます。
まとめ
JavaScriptの待機処理について、基本から実践まで解説しました。
重要なポイントをまとめてみましょう。
- setTimeoutは単純な時間待機に使うが、正確性は保証されない
- Promiseを使うと処理の完了を確実に待てる
- async/awaitで読みやすい非同期コードが書ける
- 実務では、エラーハンドリングとリトライ処理が重要
- パフォーマンスとユーザー体験のバランスを考慮する
待機処理は、快適なWebアプリケーションを作るために欠かせない技術です。
適切に実装することで、ユーザーにストレスを与えることなく、複雑な処理を実現できます。
ぜひ実際のプロジェクトで試してみてください。
著者について

とまだ
フルスタックエンジニア
Learning Next の創設者。Ruby on Rails と React を中心に、プログラミング教育に情熱を注いでいます。初心者が楽しく学べる環境作りを目指しています。
著者の詳細を見る →