キャッシュ
Cache Storage API でレスポンスを保存・再利用。Cache First / Network First / Stale While Revalidate など コンテンツの種類に応じた戦略を組み合わせる。
Cache API の基本
// 開く(無ければ作成)
const cache = await caches.open("v1")
// 単一の URL を取って入れる
await cache.add("/styles.css")
// 複数まとめて
await cache.addAll(["/", "/app.js", "/styles.css"])
// 任意の Response を入れる
await cache.put("/api/data", new Response(JSON.stringify({ ok: true })))
// 取得
const res = await cache.match("/styles.css")
// 削除
await cache.delete("/old-asset.js")
// キャッシュ全体を削除
await caches.delete("v1")
// すべてのキャッシュを列挙
const keys = await caches.keys()
主な戦略
1. Cache First(オフライン優先)
キャッシュにあれば返す、無ければ network。静的ファイル(CSS / JS / 画像)に最適。
async function cacheFirst(req) {
const cached = await caches.match(req)
if (cached) return cached
const fresh = await fetch(req)
const cache = await caches.open("static")
cache.put(req, fresh.clone())
return fresh
}
2. Network First(最新優先)
まず network、失敗したらキャッシュ。HTML / API レスポンスに。
async function networkFirst(req) {
try {
const fresh = await fetch(req)
const cache = await caches.open("dynamic")
cache.put(req, fresh.clone())
return fresh
} catch {
const cached = await caches.match(req)
return cached || new Response("Offline", { status: 503 })
}
}
3. Stale While Revalidate
まずキャッシュを返して、裏で network から取り直してキャッシュ更新。 速度と新鮮さの折衷。アバター画像・ニュース等に。
async function staleWhileRevalidate(req) {
const cache = await caches.open("v1")
const cached = await cache.match(req)
const fetching = fetch(req).then((res) => {
cache.put(req, res.clone())
return res
})
return cached || fetching
}
4. Network Only
async function networkOnly(req) {
return fetch(req)
}
5. Cache Only
async function cacheOnly(req) {
const cached = await caches.match(req)
if (cached) return cached
return new Response(null, { status: 504 })
}
戦略の使い分け
| コンテンツ | 戦略 |
|---|---|
| HTML(メイン) | Network First or NetworkOnly |
| CSS / JS(バージョン付き URL) | Cache First |
| CSS / JS(バージョン無し) | Network First |
| 画像(アセット) | Cache First |
| API(変動) | Network First or SWR |
| API(不変) | Cache First |
| フォント | Cache First(永続) |
パスごとに戦略を分ける
self.addEventListener("fetch", (e) => {
const url = new URL(e.request.url)
// HTML は network first
if (e.request.mode === "navigate") {
e.respondWith(networkFirst(e.request))
return
}
// 画像は cache first
if (e.request.destination === "image") {
e.respondWith(cacheFirst(e.request))
return
}
// API は SWR
if (url.pathname.startsWith("/api/")) {
e.respondWith(staleWhileRevalidate(e.request))
return
}
// それ以外
e.respondWith(cacheFirst(e.request))
})
事前キャッシュ(Precache)
SW install 時に必須リソースをまとめて取得しておく。
const PRECACHE = "precache-v1"
const PRECACHE_URLS = [
"/",
"/app.js",
"/styles.css",
"/icons/icon-192.png",
"/offline.html",
]
self.addEventListener("install", (e) => {
e.waitUntil(
caches.open(PRECACHE).then((c) => c.addAll(PRECACHE_URLS))
)
})
キャッシュのバージョニング
デプロイで内容が変わったらキャッシュキーを変える。古い物は activate で削除。
const VERSION = "v3"
const CACHES = {
static: `static-${VERSION}`,
dynamic: `dynamic-${VERSION}`,
}
self.addEventListener("activate", (e) => {
e.waitUntil((async () => {
const keys = await caches.keys()
const valid = Object.values(CACHES)
await Promise.all(keys.filter((k) => !valid.includes(k)).map((k) => caches.delete(k)))
})())
})
キャッシュサイズの管理
async function trimCache(name, max) {
const cache = await caches.open(name)
const keys = await cache.keys()
if (keys.length > max) {
await cache.delete(keys[0]) // FIFO
await trimCache(name, max)
}
}
// 画像キャッシュは 50 件まで
e.waitUntil(trimCache("images", 50))
StorageManager で容量確認
const { quota, usage } = await navigator.storage.estimate()
console.log(`${(usage / 1024 / 1024).toFixed(1)} / ${(quota / 1024 / 1024).toFixed(1)} MB`)
// 永続化を要求
const persisted = await navigator.storage.persist()
Workbox(推奨)
Google のSW ライブラリ。戦略が組み込みで、precache の自動更新もしてくれる。
import { registerRoute } from "workbox-routing"
import { CacheFirst, NetworkFirst, StaleWhileRevalidate } from "workbox-strategies"
import { ExpirationPlugin } from "workbox-expiration"
import { precacheAndRoute } from "workbox-precaching"
precacheAndRoute(self.__WB_MANIFEST) // ビルド時に注入
registerRoute(
({ request }) => request.destination === "image",
new CacheFirst({
cacheName: "images",
plugins: [
new ExpirationPlugin({ maxEntries: 50, maxAgeSeconds: 30 * 24 * 60 * 60 }),
],
}),
)
registerRoute(
({ url }) => url.pathname.startsWith("/api/"),
new NetworkFirst({ cacheName: "api", networkTimeoutSeconds: 5 }),
)
registerRoute(
({ request }) => request.mode === "navigate",
new NetworkFirst({ cacheName: "pages" }),
)
vite-plugin-pwa / next-pwa / serwist 経由で使うのが楽。
キャッシュの破棄
ユーザーから「キャッシュが古い」相談が来たら、SW から手動で:
// クライアント側
async function clearAllCaches() {
const keys = await caches.keys()
await Promise.all(keys.map((k) => caches.delete(k)))
if (navigator.serviceWorker.controller) {
const reg = await navigator.serviceWorker.getRegistration()
await reg?.unregister()
}
location.reload()
}
HTML をキャッシュすると古いバージョンが残り続ける地獄になりやすい。
✓ HTML は NetworkFirst + networkTimeoutSeconds 短め
✓ 静的アセットはファイル名にハッシュを入れて Cache First
✓ キャッシュキーはバージョン付きにする