Webアプリを爆速にし、オフラインでも動作させる魔法の技術、それがService Workerです。
この記事では、Service Workerの心臓部であるキャッシュ戦略について、具体的なコードと実例で解説しています。
なぜinstall、fetch、activateという3つのイベントが必要なのかについても解説しています。
そもそもService Workerとは何か?については下記をご参考ください。
Service Workerのキャッシュ戦略とは何か?
Service Workerを使うとなんでもかんでもキャッシュするわけではありません。膨大なWEBサイトの場合全てのページをキャッシュすると大量の保存容量を消費してしまいます。
そこで「このページやアイコンはキャッシュするけど、これはしない」という設定をすることができます。これがService Workerのキャッシュ戦略です。
もう少し専門的に言うと、ブラウザがネットワークからリソースを取得する際、そのリクエストをService Workerが受け取り、キャッシュを使うかネットワークを使うか、あるいはその両方をどのように組み合わせるかを決めるルールのことです。
キャッシュ戦略を理解するには、Service WorkerのJavaScriptファイルに記述する、install, active, fetchイベントの理解が欠かせません。
install, active, fetch
Service Workerは、「install」「active」「fetch」という3つの主要なイベントを通じてキャッシュを管理します。
installイベント
installイベントは、Service Workerがブラウザにインストールされるときに一度だけ実行する処理です
ここで、アプリの基本的な動作に必要なリソースを事前にキャッシュしておきます。これを「プリキャッシュ」と呼びます。
キャッシュ戦略においては、installの処理の中で絶対にキャッシュして欲しいファイルを指定します。(オフラインで使用したいファイルや共通ロゴなど)
activateイベント
activeイベントでは古いキャッシュをどう処理するかや、新しいService Workerへの移行方法を指定します。
以前のバージョンのService Workerが残した不要なキャッシュを削除し、ディスク容量を節約するコードを記述します。
Service Workerが新しく更新されたとき(例: CACHE_NAMEをv1からv2に変更したとき)に実行されます。古いService Workerが管理していた不要になったキャッシュを削除するのが主な役割です。
fetchイベント
fetchイベントでは、ネットワークリクエストを横取りし、適用するキャッシュ戦略を記述します。
installとactivateで準備・整理されたキャッシュを使って、どのようにリクエストに応答するかを制御します。
installとactiveは共通
基本的にキャッシュ戦略はfetchの部分に記述します。
installとactiveのコードは各キャッシュ戦略で共通となります。
// キャッシュのバージョン名。新しいバージョンではこれを変更する。
const STATIC_CACHE_NAME = 'static-v1';
const DYNAMIC_CACHE_NAME = 'dynamic-v1'; // 動的キャッシュ(fetchで保存されるもの)用
const CACHES = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];
// 1. install: プリキャッシュ(App Shell)の準備
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(STATIC_CACHE_NAME).then((cache) => {
// オフラインでも表示したい最小限のリソースを事前にキャッシュ(プリキャッシュ)
return cache.addAll([
'/',
'/styles/app.css',
'/scripts/app.js',
'/images/logo.png',
'/offline.html' // 共通のオフラインページ
]);
})
);
});
// 2. activate: 古いキャッシュの削除
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 現在使用するキャッシュ名(STATIC_CACHE_NAMEとDYNAMIC_CACHE_NAME)以外のキャッシュを削除
if (!CACHES.includes(cacheName)) {
return caches.delete(cacheName);
}
})
);
})
);
});installのcache.addAllで配列内で指定したパスに該当するデータがプリキャッシュされます。
キャッシュ領域の名前
const STATIC_CACHE_NAME = 'static-v1';
const DYNAMIC_CACHE_NAME = 'dynamic-v1'; // 動的キャッシュ(fetchで保存されるもの)用
const CACHES = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];キャッシュ領域名を格納した変数、STATIC_CACHE_NAMEは開発者が自由に決められる変数名ですが、その値(文字列)は、Service Workerがブラウザのキャッシュ領域に作る「保管庫(キャッシュストレージ)」の名前そのものになります。
つまり上記の場合、static-v1やdynamic-v1 という文字列が、キャッシュストレージ内に作成される実際のキャッシュ領域の名前になります。
Service Workerは、caches.open() メソッドを使って、キャッシュ領域を開き、ファイルを入れます。
なお、バージョン番号(-v1 や -v2)を付ける最大の理由は、「古いキャッシュの削除」を安全に行うためです。
もしキャッシュ名が常に static のままだと、新しい Service Worker は古いキャッシュを上書きしようとしますが、ファイルの更新漏れやエラーがあった場合、不完全なキャッシュが残ってしまいます。
コードの例でstatic-v1やdynamic-v1のようにキャッシュ領域を2つに分けている理由は、static-v1はService Workerの登録(installイベント)で常に保存しておくファイルを保存するためのものです。
static-v1はfetchイベントで取得したファイルを保存するための領域です。
キャッシュ戦略の種類
Service Workerのキャッシュ戦略は何を保存するかや用途に合わせて主に5種類あります。
| 戦略名 | 概要 | 特徴 | キャッシュするもの |
|---|---|---|---|
| Cache Only | キャッシュのみ使用。ネットワークは無視。 | 最速・オフライン | アプリシェルの静的ファイル(ロゴ、コアCSS/JS) |
| Network Only | ネットワークのみ使用。キャッシュは無視。 | 最新性 | APIリクエスト、アクセス解析データ(ログ) |
| Cache First | 最初にキャッシュを探し、なければネットワークへ。 | オフライン・高速性 | ハッシュ付きの静的リソース(フォント、画像) |
| Network First | 最初にネットワークを探し、失敗したらキャッシュへ。 | 最新性・フォールバック | HTMLファイル、頻繁に更新されるデータ |
| Stale-While-Revalidate | キャッシュを即座に返し、同時にネットワークで裏側で更新する。 | 体感的な速さ | アバター画像など、古くても許容できるリソース |
Cache Only(キャッシュのみ)
この戦略は、リソースがプリキャッシュされていることを前提とし、ネットワークに一切アクセスせず、キャッシュからのみ応答を試みます。
つまり、リソースがキャッシュにあればそれを返し、なければネットワークに行かず、リクエストは失敗(オフラインエラーのような状態)します。
ただし、以下のように条件を限定するのが一般的です。
self.addEventListener('fetch', (event) => {
// 事前にプリキャッシュしたコアなリソースのみに適用
if (event.request.url.includes('/styles/app.css')) {
event.respondWith(
// キャッシュに存在するもののみを返す。なければエラー(接続エラーのように見える)
caches.match(event.request)
);
return;
}
});リクエストされたURLに /styles/app.css が含まれている場合のみService Workerのキャッシュチェックを行います。
Network Only(ネットワークのみ)
この戦略は、常に最新のデータを必要とし、キャッシュの利用を完全に無視します。
self.addEventListener('fetch', (event) => {
// 例: /api/live/ へのリクエストに適用
if (event.request.url.includes('/api/live/') || event.request.method !== 'GET') {
// Service Workerを介さず、ネットワークにそのままリクエストを流す
return;
}
// 他のリクエストは通常の処理へ
});event.respondWith() が呼ばれていないため、Service Worker はこのリクエストに関与せず、ブラウザはデフォルトでネットワークにアクセスします。
Cache First, Falling Back to Network(キャッシュ優先、ネットワークへフォールバック)
この戦略は、高速なアクセスとオフライン対応を目的とし、キャッシュを使いつつ、キャッシュミスの場合にネットワークに頼ります。
2回目以降のアクセスは高速なキャッシュから。初回アクセス時はネットワークから取得し、キャッシュが作成されます。
self.addEventListener('fetch', (event) => {
// 例: 画像ファイル(.jpg)に適用
if (event.request.url.includes('.jpg')) {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
// 1. キャッシュにあればそれを返す (Cache First)
if (cachedResponse) {
return cachedResponse;
}
// 2. なければネットワークへアクセス (Falling Back to Network)
return fetch(event.request).then((networkResponse) => {
// 3. ネットワーク成功時に動的キャッシュに保存
return caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
})
);
}
});Network First, Falling Back to Cache(ネットワーク優先、キャッシュへフォールバック)
最新の情報が重要ですが、オフラインでも何らかのデータを提供したい場合に利用します。
オンラインなら最新データ、オフラインなら前回取得したデータまたはオフラインページが表示されます。
self.addEventListener('fetch', (event) => {
// 例: HTMLナビゲーション(ドキュメントリクエスト)に適用
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request)
.then((networkResponse) => {
// 1. ネットワーク成功: キャッシュを更新して最新を返す
return caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
})
.catch(() => {
// 2. ネットワーク失敗: キャッシュからフォールバック
return caches.match(event.request).then((cachedResponse) => {
// キャッシュにもなければ、オフラインページを返すなど
return cachedResponse || caches.match('/offline.html');
});
})
);
}
});Stale-While-Revalidate(古いが検証中に新しいものを取得)
体感速度を重視する戦略です。キャッシュがあればすぐに返し、同時に裏でネットワークを検証します。
ユーザーは常に最速で画面を見始められます。古いデータを見た場合でも、裏側でキャッシュが更新されるため、次回アクセス時には最新に近いデータが表示されます。
self.addEventListener('fetch', (event) => {
// 例: APIデータ(/api/data/)に適用
if (event.request.url.includes('/api/data/')) {
// ネットワークリクエストを実行するPromise (裏側の処理)
const networkFetch = fetch(event.request)
.then((networkResponse) => {
// ネットワーク成功時にキャッシュを更新(次回利用のため)
return caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
});
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
// 1. キャッシュがあれば即座に返す (Stale)
if (cachedResponse) {
// 2. ネットワークの検証は裏側で続行 (Revalidate)
return cachedResponse;
}
// 3. キャッシュがなければネットワークの結果を待つ
return networkFetch;
})
);
}
});戦略に合わせ複数を混ぜ合わせる
Service Workerの5つのキャッシュ戦略は、どれか一つを選ぶものではなく、Webサイトを構成するリソースの種類(HTML、CSS、画像、APIデータなど)や特性(更新頻度、重要度)に応じて、fetchイベント内で if文や条件分岐を使って細かく使い分けるのが一般的かつ最適なプラクティスとなります。
1つの戦略だけを使う問題点
全てのリソースを同じ方法でキャッシュすると、必ず問題が生じます。
例えば全てのリソースをCache Firstにすると、HTMLファイルが更新されず、ユーザーに古い情報が表示され続けます。
逆に、全てのリソースをNetwork Firstにすると、CSSやロゴ画像のような静的ファイルでも毎回ネットワークアクセスが発生し、サイトの表示が遅くなります。
適用例
| リソースの種別 | 判定方法(if文の条件) | キャッシュ戦略 |
|---|---|---|
| HTMLドキュメント | event.request.mode === ‘navigate’ | Network First |
| 静的アセット (バージョン付きCSS/JS) | event.request.url.includes(‘.css’) や event.request.url.includes(‘.js’) | Cache First または Cache Only |
| ユーザー生成画像 | event.request.url.includes(‘/avatar/’) | Stale-While-Revalidate |
| リアルタイムAPI | event.request.url.includes(‘/api/live/’) | Network Only |
実例
5つのキャッシュ戦略をファイルの種類に応じて使い分ける、Service Workerのfetchイベントの統合コードは以下のようになります。
このコードは、リクエストのURLやモードを見て、最適な戦略を選択する「ルーター」として機能します。
// Service Workerファイル (sw.js) 内の fetch イベント
// キャッシュ名の定義(install/activateイベントで定義したものと同じものを使用)
const STATIC_CACHE_NAME = 'static-v1';
const DYNAMIC_CACHE_NAME = 'dynamic-v1';
self.addEventListener('fetch', (event) => {
const request = event.request;
const url = new URL(request.url);
// 1. Network Only (リアルタイムAPIやPOSTリクエストなど)
// GETリクエストではない、またはリアルタイムAPIの場合
if (request.method !== 'GET' || url.pathname.includes('/api/live/')) {
// Service Workerを通過させ、ネットワークに直接アクセスさせる
return;
}
// 2. Cache Only (プリキャッシュされたコアな静的アセット)
// 例: バージョンアップ時以外変わらないコアなCSSファイル
if (url.pathname.includes('/styles/app.css')) {
event.respondWith(
caches.match(request) // キャッシュのみを検索
);
return;
}
// 3. Network First, Falling Back to Cache (HTMLなど、最新情報が重要なもの)
// 'navigate'モードは、ブラウザが新しいページに移動する際のリクエスト(通常はHTML)を指す
if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then((networkResponse) => {
// ネットワーク成功: キャッシュを更新して最新を返す
return caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
// キャッシュに入れるためにレスポンスを複製 (.clone()が必要)
cache.put(request, networkResponse.clone());
return networkResponse;
});
})
.catch(() => {
// ネットワーク失敗: キャッシュからフォールバック
return caches.match(request).then((cachedResponse) => {
// キャッシュにもなければ、プリキャッシュしたオフラインページを返す
return cachedResponse || caches.match('/offline.html');
});
})
);
return;
}
// 4. Stale-While-Revalidate (アバター画像や頻繁に更新されるが即時性も大事なデータ)
// 例: APIデータやユーザー画像
if (url.pathname.includes('/api/data/') || url.pathname.includes('/images/user/')) {
// ネットワークリクエストを開始し、結果をPromiseとして保持
const networkFetch = fetch(request).then((networkResponse) => {
// 裏でキャッシュを更新
return caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
});
event.respondWith(
caches.match(request).then((cachedResponse) => {
// キャッシュにあれば即座に返し (Stale)、裏で networkFetch を続行 (Revalidate)
if (cachedResponse) {
return cachedResponse;
}
// キャッシュになければネットワークの結果を待つ
return networkFetch;
})
);
return;
}
// 5. Cache First, Falling Back to Network (デフォルトの静的アセット)
// 上記のどの条件にも当てはまらない、一般的な画像、スクリプト、フォントなどに適用
event.respondWith(
caches.match(request).then((cachedResponse) => {
// 1. キャッシュにあればそれを返す
if (cachedResponse) {
return cachedResponse;
}
// 2. なければネットワークへアクセス
return fetch(request).then((networkResponse) => {
// 3. ネットワーク成功時に動的キャッシュに保存
// ステータスコードが 200 であるか、opaque response (クロスオリジン) でないか確認することが望ましい
if (!networkResponse || networkResponse.status !== 200 || networkResponse.type !== 'basic') {
return networkResponse;
}
return caches.open(DYNAMIC_CACHE_NAME).then((cache) => {
cache.put(request, networkResponse.clone());
return networkResponse;
});
});
})
);
});上から順に条件をチェックし、いずれの条件にも当てはまらなかったリクエストに対しては、最も一般的なキャッシュ戦略である Cache First, Falling Back to Network を適用するように記述しています。
それぞれの if ブロック内で event.respondWith() が実行されたら、そのリクエストの処理はそこで完了し、return; によって他の条件チェックに進むのを防いでいます。

