waitUntilとは?10選の使用する方法と使用時の注意点も解説

JavaScriptのwaitUntilを使った非同期処理の制御方法JS
この記事は約23分で読めます。

※本記事のコンテンツは、利用目的を問わずご活用いただけます。実務経験10000時間以上のエンジニアが監修しており、基礎知識があれば初心者にも理解していただけるように、常に解説内容のわかりやすさや記事の品質に注力しております。不具合・分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。(理解できない部分などの個別相談も無償で承っております)
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

●waitUntilとは?

JavaScriptでは、非同期処理がとても重要な役割を果たしています。

ユーザーとのインタラクションを損なうことなく、バックグラウンドで処理を進められるのが非同期処理の魅力ですよね。

でも、時として非同期処理を適切に制御しないと、思わぬバグやパフォーマンスの低下を招いてしまうこともあるんです。

そこで登場するのが、waitUntilというメソッド。

これは非同期処理を待機するための強力な武器なんです。

特にServiceWorkerやWebWorkerを使う場面で、waitUntilはとても重宝します。

○非同期処理と同期処理の違い

非同期処理と同期処理の違いって、結構わかりにくいですよね。

簡単に言うと、同期処理は一つのタスクが完了するまで次のタスクに進まないのに対し、非同期処理ではタスクの完了を待たずに次のタスクを実行できるんです。

JavaScriptではsetTimeoutやPromise、async/awaitなどを使って非同期処理を実現しています。

でも、非同期処理を使いこなすのはちょっとハードルが高い。

処理の順序が保証されないから、思わぬところでバグが発生したりするんですよね。

だからこそ、waitUntilのような非同期処理を制御するための仕組みが必要なんです。

○waitUntilの役割

waitUntilは、指定した非同期処理が完了するまで待機するためのメソッドです。

これを使えば、複雑な非同期処理の流れの中で「ここまでは確実に処理を終えてから次に進みたい」というポイントを制御できるんです。

たとえばServiceWorkerの中で、イベントリスナーの登録やキャッシュの更新など、起動時にやっておきたい処理があるとします。

でも、それらの処理が完了する前にServiceWorkerが終了しちゃうと、せっかくの処理が無駄になってしまう。

そんな時はwaitUntilを使って、ServiceWorkerの終了を遅らせることができるんです。

○サンプルコード1:Promiseを使ったwaitUntil

waitUntilの使い方を、Promiseを使ったサンプルコードで見てみましょう。

someAsyncFunction().then(() => {
  // 非同期処理が完了した後の処理
});

event.waitUntil(someAsyncFunction());

ここではeventオブジェクトのwaitUntilメソッドを使って、someAsyncFunction()の完了を待っています。

こうすることで、someAsyncFunction()が完了するまでeventの処理が終了しないようになるんです。

実行結果としては、someAsyncFunction()の処理が完了してから、then以下の処理が実行されることになります。

これが、waitUntilを使った非同期処理の制御の基本的な流れですね。

●waitUntilの使用例

さて、waitUntilの基本的な使い方はわかってきたと思います。

では実際に、どのようなシーンでwaitUntilが活躍するのでしょうか。

ここからは、waitUntilの具体的な使用例を見ていきましょう。

○ServiceWorkerでのwaitUntil

ServiceWorkerは、ブラウザのバックグラウンドで動作するスクリプトで、Webアプリケーションのオフライン対応やプッシュ通知などを実現するのに欠かせない存在です。

そんなServiceWorkerの中でも、waitUntilは特に重要な役割を果たします。

ServiceWorkerにはライフサイクルがあり、インストール時やアクティベーション時に行いたい処理があるんですよね。

でも、それらの処理が完了する前にServiceWorkerが終了しちゃうと、せっかくの処理が無駄になってしまう。

そこでwaitUntilの出番です。

□サンプルコード2:ServiceWorkerのインストール時

まずは、ServiceWorkerのインストール時にwaitUntilを使う例を見てみましょう。

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll([
        '/css/style.css',
        '/js/main.js',
        '/images/logo.png'
      ]);
    })
  );
});

ここでは、ServiceWorkerのインストール時に、必要なリソースをキャッシュに追加する処理を行っています。

event.waitUntilを使うことで、キャッシュへの追加が完了するまでインストールを完了しないようにしているんです。

実行結果としては、指定したリソースがキャッシュに追加されます。

もしwaitUntilを使わなかったら、キャッシュへの追加が終わる前にServiceWorkerのインストールが完了してしまい、リソースがキャッシュされない可能性があるんですよね。

□サンプルコード3:ServiceWorkerのアクティベーション時

次は、ServiceWorkerのアクティベーション時にwaitUntilを使う例です。

self.addEventListener('activate', function(event) {
  event.waitUntil(
    caches.keys().then(function(cacheNames) {
      return Promise.all(
        cacheNames.map(function(cacheName) {
          if (cacheName !== 'my-cache') {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

ここでは、ServiceWorkerのアクティベーション時に、不要になったキャッシュを削除する処理を行っています。

event.waitUntilを使うことで、キャッシュの削除が完了するまでアクティベーションを完了しないようにしているんです。

実行結果としては、’my-cache’以外のキャッシュが削除されます。

古いキャッシュを残したままだと、ストレージを圧迫してしまうので、こまめに不要なキャッシュを削除するのは大切ですよね。

○WebWorkerでのwaitUntil

ServiceWorkerと並んで、waitUntilが活躍するのがWebWorkerです。

WebWorkerは、メインスレッドとは別のバックグラウンドスレッドで重い処理を実行できる仕組みなんですよね。

でも、せっかくバックグラウンドで処理しても、メインスレッドがそれを待ってられなきゃ意味がない。

そこでwaitUntilの出番です。

WebWorkerの中でwaitUntilを使えば、バックグラウンドの処理が完了するまでメインスレッドを待機させることができるんです。

これによって、WebWorkerの処理結果を確実に受け取ることができるようになります。

□サンプルコード4:WebWorkerの処理待機

それでは実際に、WebWorkerでwaitUntilを使う例を見てみましょう。

// メインスレッド側
const worker = new Worker('worker.js');
worker.postMessage('start');

worker.addEventListener('message', function(event) {
  console.log(event.data);
});

// WebWorker側 (worker.js)
self.addEventListener('message', function(event) {
  const result = heavyProcessing();
  event.waitUntil(
    new Promise(function(resolve) {
      setTimeout(function() {
        self.postMessage(result);
        resolve();
      }, 1000);
    })
  );
});

function heavyProcessing() {
  // 重い処理
  return 'Done!';
}

ここでは、メインスレッドからWebWorkerにメッセージを送り、重い処理を依頼しています。

WebWorker側では、event.waitUntilを使って、処理結果のメッセージを送信するまで待機するようにしています。

実行結果としては、1秒後に’Done!’というメッセージがメインスレッドに送られます。

もしwaitUntilを使わなかったら、WebWorkerが終了してしまい、メッセージが送られない可能性があるんですよね。

○その他の使用例

ServiceWorkerやWebWorker以外にも、waitUntilが役立つシーンはたくさんあります。

indexedDBを使ったデータの読み書きや、バックグラウンド同期の処理なども、waitUntilを使うことで適切に制御できるんです。

□サンプルコード5:indexedDBの処理待機

たとえばindexedDBを使う場合、データの読み書きが完了するまでwaitUntilで待機するのは常套手段ですよね。

function saveData(data) {
  return new Promise(function(resolve, reject) {
    const request = indexedDB.open('myDatabase', 1);
    request.onerror = function() {
      reject(new Error('Failed to open database'));
    };
    request.onsuccess = function() {
      const db = request.result;
      const transaction = db.transaction(['myStore'], 'readwrite');
      const store = transaction.objectStore('myStore');
      const putRequest = store.put(data, 'key');
      putRequest.onerror = function() {
        reject(new Error('Failed to save data'));
      };
      putRequest.onsuccess = function() {
        resolve();
      };
    };
  });
}

event.waitUntil(saveData({ foo: 'bar' }));

ここでは、indexedDBを使ってデータを保存する処理を、event.waitUntilで待機しています。

こうすることで、データの保存が完了するまでイベントの処理を終えないようにしているんです。

実行結果としては、’myDatabase’というデータベースの’myStore’というストアに、{ foo: ‘bar’ }というデータが’key’というキーで保存されます。

indexedDBの処理は非同期なので、waitUntilを使わないと、保存が完了する前にイベントの処理が終わってしまう可能性があります。

●waitUntilの注意点

さて、ここまでwaitUntilの使い方やメリットについて見てきましたが、使う上での注意点もしっかり押さえておく必要がありますよね。

waitUntilを使えば何でもうまくいくわけではなく、時として思わぬ落とし穴があるものなんです。

○タイムアウトに注意

まず注意したいのが、waitUntilにはタイムアウトの概念がないということ。

つまり、waitUntilに渡した処理が永遠に完了しなければ、ずっと待ち続けてしまいます。

これでは、かえってユーザー体験を損ねてしまいかねません。

ですから、waitUntilを使う際は、適切なタイムアウト処理を自分で実装する必要があります。

Promiseのraceメソッドを使うのが定番の手法ですね。

○エラーハンドリングを忘れずに

もう一つ忘れてはいけないのが、エラーハンドリングです。

waitUntilに渡した処理が途中で失敗した場合、それをキャッチしてきちんと処理しないと、予期せぬ動作につながります。

特にServiceWorkerの場合、ライフサイクルに関わる重要な処理をwaitUntilで制御することが多いので、エラーハンドリングはしっかりとやっておきたいところですよね。

○サンプルコード6:タイムアウトとエラーハンドリング

それでは実際に、タイムアウトとエラーハンドリングを実装したwaitUntilの例を見てみましょう。

function waitUntilTimeout(promise, timeoutMillis) {
  const timeoutPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('Timeout'));
    }, timeoutMillis);
  });

  return Promise.race([promise, timeoutPromise]);
}

self.addEventListener('install', function(event) {
  event.waitUntil(
    waitUntilTimeout(
      caches.open('my-cache').then(function(cache) {
        return cache.addAll([
          '/css/style.css',
          '/js/main.js',
          '/images/logo.png'
        ]);
      }),
      5000
    ).catch(function(error) {
      console.error('Error in waitUntil:', error);
      // 適切なエラー処理を行う
    })
  );
});

ここでは、waitUntilTimeoutという関数を定義して、Promiseにタイムアウト処理を追加しています。

そしてcatch節で、エラーをキャッチして適切に処理するようにしているんです。

実行結果としては、caches.openやcache.addAllが5秒以内に完了すれば、通常通りキャッシュが追加されます。

もし5秒経ってもまだ完了しなければ、’Timeout’エラーが発生し、catch節に処理が移ります。

●よくあるエラーと対処法

waitUntilを使っていると、時々mysteriousなエラーに遭遇することがあります。

特に初めて使う時は、「何これ?」と頭を抱えてしまうことも。

でも大丈夫、よくあるエラーとその対処法を知っておけば、そんなトラブルともサヨナラできるはずです。

○”waitUntil() can only be called once.”

これは、waitUntilを同じイベントに対して複数回呼び出そうとした時に発生するエラーですね。

waitUntilは、一つのイベントに対して一度しか使えないというルールがあるんです。

例えば、ServiceWorkerのインストールイベントで、waitUntilを2回呼び出すとこのエラーが発生します。

self.addEventListener('install', function(event) {
  event.waitUntil(fetch('/data.json'));
  event.waitUntil(caches.open('my-cache')); // ここでエラー
});

対処法としては、シンプルに waitUntilの呼び出しを一度にまとめるだけ。

Promiseのチェーンを使えば、複数の非同期処理を一つのwaitUntilで扱えますからね。

self.addEventListener('install', function(event) {
  event.waitUntil(
    fetch('/data.json')
      .then(function() {
        return caches.open('my-cache');
      })
  );
});

○”DOMException: The service worker is in a terminated state.”

こちらは、すでに終了したServiceWorkerに対してwaitUntilを呼び出そうとした時のエラーです。

ServiceWorkerは、一定時間アイドル状態が続くと自動的に終了されるんですよね。

例えば、ServiceWorkerのアクティベーション後にsetTimeoutでwaitUntilを呼び出すと、このエラーが起きる可能性があります。

self.addEventListener('activate', function(event) {
  setTimeout(function() {
    event.waitUntil(clients.claim()); // ServiceWorkerが終了済みならエラー
  }, 5000);
});

対処法としては、ServiceWorkerの終了をコントロールすることが大切です。

terminateを呼び出して明示的に終了するか、メッセージングを使ってServiceWorkerの状態を監視するといった方法がありますね。

○”NotFoundError: Failed to execute ‘waitUntil’ on ‘ServiceWorkerGlobalScope'”

これは、ServiceWorker以外のスコープで waitUntilを呼び出した時に起きるエラーです。

waitUntilはServiceWorkerだけで使える特別なメソッドで、通常のスクリプトでは使えないんですよね。

例えば、メインスレッドのスクリプトでwaitUntilを呼び出すとこのエラーが発生します。

// メインスレッドのスクリプト
fetch('/data.json').then(function(response) {
  event.waitUntil(response.json()); // ServiceWorkerスコープ外なのでエラー
});

対処法は簡単で、ServiceWorkerの中でだけwaitUntilを使うようにするだけ。

メインスレッドとの通信が必要なら、postMessageを使うのがいいでしょう。

●waitUntilの応用例

さて、ここまでwaitUntilの基本的な使い方や注意点について見てきましたが、実際の開発ではもっと複雑な場面でwaitUntilを活用することも多いですよね。

ちょっと応用的な使い方を知っておくと、より柔軟に非同期処理を制御できるようになります。

○サンプルコード7:複数の非同期処理を待機

たとえば、複数の非同期処理を並行して実行し、それら全ての完了を待ってから次の処理に進みたいという場面があります。

そんな時は、Promise.allを使ってwaitUntilに渡すのが便利ですね。

function fetchAssets() {
  return Promise.all([
    fetch('/data1.json'),
    fetch('/data2.json'),
    fetch('/data3.json')
  ]);
}

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll([
        '/app.js',
        '/style.css'
      ]);
    }).then(fetchAssets)
  );
});

このコードでは、キャッシュへのアセットの追加が完了した後に、fetchAssets関数で複数のデータを並行して取得しています。

fetchAssets関数ではPromise.allを使っているので、全てのfetchが完了するまで待機されます。

実行結果としては、キャッシュへのアセット追加と、3つのデータの取得が完了してから、ServiceWorkerのインストールが完了することになります。

複数の非同期処理を扱う場合でも、waitUntilとPromiseをうまく組み合わせることで、処理の流れを適切に制御できます。

○サンプルコード8:条件付きの待機

また、場合によっては条件付きでwaitUntilを使いたいこともあるでしょう。

たとえば、キャッシュがあればそれを使い、なければ新たに取得するといった処理ですね。

function fetchWithCache(url) {
  return caches.match(url).then(function(response) {
    if (response) {
      return response;
    }
    return fetch(url).then(function(response) {
      if (response.ok) {
        return caches.open('my-cache').then(function(cache) {
          return cache.put(url, response.clone()).then(function() {
            return response;
          });
        });
      }
      return response;
    });
  });
}

self.addEventListener('fetch', function(event) {
  if (/\.jpg$/.test(event.request.url)) {
    event.respondWith(
      fetchWithCache(event.request)
    );
  } else {
    event.respondWith(
      fetch(event.request)
    );
  }
});

このコードでは、jpgファイルに対するリクエストの場合のみ、fetchWithCache関数でキャッシュの存在をチェックしています。

キャッシュがあればそれを使い、なければ新たに取得してキャッシュに追加するという処理ですね。

実行結果としては、初回アクセス時はキャッシュがないので新たにjpgファイルを取得し、キャッシュに追加します。

二回目以降のアクセスではキャッシュから読み込むので、より高速に画像を表示できるようになるんです。

○サンプルコード9:ProgressiveWebApp(PWA)での活用

ProgressiveWebApp(PWA)では、waitUntilをフル活用することで、より高度なオフライン対応や高速化を実現できます。

たとえば、動的なデータをキャッシュに保存しておき、オフラインでもアプリを使えるようにするなんてこともできてしまいます。

function fetchData() {
  return fetch('/api/data').then(function(response) {
    return response.json();
  }).then(function(data) {
    return caches.open('my-cache').then(function(cache) {
      return cache.put('/api/data', new Response(JSON.stringify(data)));
    });
  });
}

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll([
        '/',
        '/app.js',
        '/style.css'
      ]);
    }).then(fetchData)
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

このコードでは、ServiceWorkerのインストール時に、アプリに必要な静的リソースをキャッシュに追加した後、fetchData関数で動的なデータを取得し、それもキャッシュに保存しています。

そしてfetchイベントでは、リクエストがあればまずキャッシュを確認し、存在すればそれを返すようにしているんです。

実行結果としては、初回アクセス時は通常通りサーバーからデータを取得しますが、その後はオフラインでもアプリが動作するようになります。

動的なデータもキャッシュされているので、オフラインでも最新のデータが表示されるんですよね。

○サンプルコード10:バックグラウンド同期の実装

ServiceWorkerの強力な機能の一つに、バックグラウンド同期があります。

これは、オフライン時にデータの送信などを行い、オンラインに戻った際に自動的にサーバーと同期するという仕組みですね。

バックグラウンド同期を実装する際にも、waitUntilは重要な役割を果たします。

self.addEventListener('sync', function(event) {
  if (event.tag === 'my-sync') {
    event.waitUntil(
      getDataFromCache().then(function(data) {
        return sendDataToServer(data);
      }).then(function() {
        return clearCache();
      })
    );
  }
});

function getDataFromCache() {
  return caches.open('my-cache').then(function(cache) {
    return cache.match('/sync-data');
  }).then(function(response) {
    if (!response) {
      return Promise.resolve(null);
    }
    return response.json();
  });
}

function sendDataToServer(data) {
  if (!data) {
    return Promise.resolve();
  }
  return fetch('/api/sync', {
    method: 'POST',
    body: JSON.stringify(data)
  });
}

function clearCache() {
  return caches.open('my-cache').then(function(cache) {
    return cache.delete('/sync-data');
  });
}

このコードでは、syncイベントのリスナーで、’my-sync’というタグの同期リクエストを処理しています。

waitUntilの中では、まずgetDataFromCache関数でキャッシュからデータを取得し、それをsendDataToServer関数でサーバーに送信した後、clearCache関数でキャッシュを削除しているんです。

実行結果としては、オフライン時にデータが保存され、オンラインに戻った際に自動的にサーバーにデータが送信されます。

データの送信が完了したら、キャッシュからデータが削除されるので、二重送信などの問題も防げます。

まとめ

これからのWebアプリケーション開発では、非同期処理の適切な制御がますます重要になってくるでしょう。

waitUntilを使いこなせるスキルを身につけておくことで、より高度で洗練されたアプリケーションを開発できるようになること間違いなしです。

ぜひ、今回紹介した使用例や注意点を参考に、waitUntilをマスターしていってくださいね。