読み込み中...

JavaScriptの非同期処理、こう書けば爆速!forEach + async/await 実践テクニック

JavaScriptのforEachとawaitを使った非同期処理のサンプルコード JS
この記事は約10分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

本記事のサンプルコードを活用して機能追加、目的を達成できるように作ってありますので、是非ご活用ください。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を満たす現役のプログラマチームによって監修されています。

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●非同期処理の仕組みを「キッチン作業」で完全理解

JavaScriptの非同期処理、「理解」するのに苦労していませんか?

今回は「キッチン作業」という誰でも理解しやすい例で、非同期処理の本質に迫っていきましょう!

○同期処理vs非同期処理を料理で例えると…

今からあなたはシェフです。

3品の料理を作るシェフです。

同期処理の場合・・・

  1. ハンバーグを焼く(15分)
  2. その間、じっと見つめる
  3. 完成したら次にサラダを作る(5分)
  4. その後、スープを温める(5分)

結果、25分かかってしまいます。

ただ、本来シェフはそんなことしませんよね。

ラーメン屋でもそうです。

非同期処理の場合・・・

  1. ハンバーグを焼き始める(15分)
  2. その間にサラダも作る(5分)
  3. 同時にスープも温める(5分)
  4. ハンバーグが焼けるのを待つ

最も時間のかかるハンバーグ15分の間にすべて終わってしまい、ハンバーグを待つ余裕ができましたね!

結果、15分です!

お分かりの通り、料理人も非同期処理を使っているんですね。

🔍 ここがポイント!

非同期処理は「待ち時間」を有効活用できます。JavaScriptでも同じ考え方が適用されます。
データの読み込みやAPI通信の待ち時間中に、他の処理を実行できるということですね。

実際のコードで見てみよう

// 同期処理の例
function cookSynchronously() {
    cookHamburg();  // 15秒待つ
    makeSalad();    // 5秒待つ
    heatSoup();     // 5秒待つ
}

// 非同期処理の例
async function cookAsynchronously() {
    const hamburgPromise = cookHamburg(); // 調理開始
    makeSalad();  // その間にサラダを作る
    heatSoup();   // スープも温める
    await hamburgPromise; // ハンバーグの完成を待つ
}

現代のWebアプリケーションでは、ユーザー体験(UX)を向上させるために非同期処理が必須となっています。

例えば、Twitterのタイムライン読み込み中でも、ユーザーは入力や他の操作ができますよね。

●『非同期のメリット』パフォーマンス向上の決め手

先述で察しのついた方も多かれ少なかれいるとは思いますが、非同期処理は現代のWebアプリケーション開発において、パフォーマンスを大きく左右する重要な要素です。

特にユーザー体験(UX)の向上とシステムリソースの効率的な利用の両面で、大きなメリットをもたらします。

実際のプロジェクトでどのように活用されているのか、具体例を交えながら詳しくなぞっていきましょう!

○なぜ非同期処理が必要なの?

ユーザー体験の向上

    • ローディング中でもUIが固まらない
    • レスポンシブな操作感を実現
    • (画像のプレビュー表示中でもフォーム入力可能)

    リソースの効率的な使用

    • CPUのアイドル時間を削減
    • 複数のタスクを並行処理
    • メモリの効率的な利用

    スケーラビリティの向上

    • 多数のリクエストを効率的に処理
    • サーバーリソースの有効活用

    🔍 ここがポイント!
    非同期処理は単なる「待ち時間の活用」だけではありません。システム全体のパフォーマンスとスケーラビリティに大きく影響してきます。

    ○コールバック地獄を脱出!Promise誕生の背景

    コールバック地獄とは?

    // コールバック地獄の例
    getData(function(a) {
        getMoreData(a, function(b) {
            getMoreData(b, function(c) {
                getMoreData(c, function(d) {
                    // 深いネストに...😱
                });
            });
        });
    });

    🚫 よくある問題点

    • コードの可読性が著しく低下
    • エラーハンドリングが複雑に
    • デバッグが困難
    • コードのメンテナンスが大変

    こんな時は、Promiseによる解決を試みましょう!

    // Promiseを使用した場合
    getData()
        .then(a => getMoreData(a))
        .then(b => getMoreData(b))
        .then(c => getMoreData(c))
        .then(d => {
            // スッキリ!😊
        })
        .catch(error => {
            // エラーハンドリングも簡単
        });

    🔍 ここがポイント!
    Promiseは非同期処理を「約束」として扱います。「処理が完了したら次はこうする」という流れを、直感的に書けるようになりましたね。

    ○さらに進化!async/await

    // async/awaitを使用した場合
    async function processData() {
        try {
            const a = await getData();
            const b = await getMoreData(a);
            const c = await getMoreData(b);
            const d = await getMoreData(c);
            // 同期処理のように書ける!🎉
        } catch (error) {
            // エラーハンドリング
        }
    }

    💡 豆知識
    async/awaitはPromiseをベースにしていますが、より同期的なコードのように書けます。
    これで、コードの見通しが良くなり、デバッグも容易になりました。

    ●forEachループの落とし穴と対処法

    非同期処理でforEachを使うとき、思わぬ落とし穴にハマった経験はありませんか?

    実は多くのエンジニアが経験するこの問題、しっかり理解して適切に対処していきましょう!

    よくあるforEachの罠的要素

    // ❌ よくある間違った実装
    async function processItems() {
        const items = [1, 2, 3, 4, 5];
        
        items.forEach(async (item) => {
            const result = await processItem(item);
            console.log(result);
        });
        
        console.log('完了!');  // 🚨 実際には処理が完了していない!
    }

    🔍 ここがポイント!
    forEachは非同期処理を待ってくれません。
    上のコードでは「完了!」が先に表示されてしまいます。

    正しい実装方法

    // ✅ 推奨される実装
    async function processItems() {
        const items = [1, 2, 3, 4, 5];
        
        // <<方法1>>for...ofを使用
        for (const item of items) {
            const result = await processItem(item);
            console.log(result);
        }
        
        // <<方法2>>Promise.allを使用(並列処理)
        await Promise.all(
            items.map(async (item) => {
                const result = await processItem(item);
                console.log(result);
            })
        );
        
        console.log('本当に完了!'); // ✨ 全ての処理完了後に実行
    }

    forEachの代わりにfor…ofを使うと、同期的な見た目のまま非同期処理を書けます。

    コードの意図が明確になりますね!

    ○なぜPromise.allを使うべきなのか

    メリットは並列実行による高速化!

    // 逐次実行:10秒かかる
    for (const id of ids) {
        await fetchData(id); // 1回2秒
    }
    
    // 並列実行:2秒で完了!
    await Promise.all(ids.map(id => fetchData(id)));

    エラーハンドリングの一元化もできる!

    try {
        await Promise.all(tasks.map(async task => {
            await processTask(task);
        }));
    } catch (error) {
        // 全てのエラーをここで捕捉
        console.error('エラー発生:', error);
    }

    🔍 ここがポイント!
    Promise.allは全ての非同期処理が成功した時のみ完了します。
    一つでも失敗すると即座にエラーを返します。

    Promise.allSettledという選択肢も

    const results = await Promise.allSettled(tasks.map(task => processTask(task)));
    
    // 成功・失敗の結果を個別に確認
    results.forEach(result => {
        if (result.status === 'fulfilled') {
            console.log('成功:', result.value);
        } else {
            console.log('失敗:', result.reason);
        }
    });

    💡 豆知識
    Promise.allSettledは、一部の処理が失敗しても他の処理を続行したい場合に便利です。
    例えば、複数の画像を一括アップロードする機能での使用が想定されます。

    ●forEachとfor-ofのパフォーマンス差

    実行速度の比較コード

    // パフォーマンステスト用のコード
    async function measurePerformance() {
        const items = Array.from({ length: 1000 }, (_, i) => i);
        
        console.time('forEach');
        await Promise.all(items.forEach(async item => {
            await someAsyncOperation(item);
        }));
        console.timeEnd('forEach');
        
        console.time('for-of');
        for (const item of items) {
            await someAsyncOperation(item);
        }
        console.timeEnd('for-of');
    }
    パターンメモリ使用量実行速度デバッグのしやすさ
    forEach🟡 中🟢 速い🔴 難しい
    for-of🟢 少ない🟡 普通🟢 簡単
    Promise.all🔴 多い🟢 最速🟡 普通

    まとめ

    今回学んだことを簡単におさらいしましょう!

    逐次処理が必要な場合

    // for-ofを使用
    for (const item of items) {
        await processItem(item);
    }

    処理順序が重要な場合はfor-ofを使いましょう。

    並列処理で高速化したい場合

    // Promise.allを使用
    await Promise.all(items.map(item => processItem(item)));

    独立した処理の並列実行にはPromise.allを使いましょう。

    エラーを個別に処理したい場合

    // Promise.allSettledを使用
    const results = await Promise.allSettled(items.map(item => processItem(item)));

    エラー処理が複雑な場合はPromise.allSettledを使いましょう。

    参考になったら嬉しいです!