2026年10月1日
2026年10月1日
WordPressのService Workerキャッシュ戦略の選び方|リソース別の使い分けと実装例
Service Worker のキャッシュ戦略は「リソースの種類ごとに、鮮度と速さのどちらを優先するか」で選びます。WordPress では、HTML はネットワーク優先、バージョン付きの CSS・JS はキャッシュ優先、画像は stale-while-revalidate を基本にし、管理画面・ログイン・REST API・カートはキャッシュしない、という分け方が扱いやすい構成です。この記事では各戦略の違いと、WordPress で事故を起こさない実装例、更新と解除の方法まで解説します。
5つのキャッシュ戦略と向いている用途
Service Worker は fetch イベントでリクエストを横取りし、Cache Storage とネットワークのどちらから応答するかを決めます。代表的な戦略は次の5つで、Google の Workbox ライブラリでも同じ名前のクラス(CacheFirst、NetworkFirst、StaleWhileRevalidate、NetworkOnly、CacheOnly)が用意されています。
- Cache First: キャッシュにあればそれを返し、無いときだけネットワークへ。速いが、更新に気づかない。URL にバージョンが付いた静的ファイル向き
- Network First: まずネットワーク、失敗したらキャッシュ。鮮度を優先しつつオフラインにも対応できる。HTML ページ向き
- Stale-While-Revalidate: キャッシュを即座に返し、裏でネットワークから取り直して次回に備える。少し古くてもよい画像やアイコン向き
- Network Only: キャッシュを使わない。管理画面、ログイン、決済、個人ごとに内容が変わるページ向き
- Cache Only: インストール時に保存したものだけを返す。オフライン用の案内ページなど限られた用途向き
選ぶときの基準は「古い内容を見せたら困るか」と「URL が変わらずに中身が変わるか」の2点です。WordPress はテーマやプラグインの CSS・JS に ?ver= を付けて出力するため、バージョンが上がれば URL が変わり、Cache First でも新しいファイルが読み込まれます。一方、投稿ページの HTML は同じ URL のまま更新されるため、Cache First にすると記事を直しても読者に古い版が表示され続けます。
WordPressでキャッシュしてはいけないもの
戦略を選ぶ前に、対象から外すものを決めておくことが大切です。ここを誤ると、ログイン中の画面が他人に見える、カートの中身が古いまま表示される、といった問題につながります。
/wp-admin/配下とwp-login.php- REST API(
/wp-json/と?rest_route=)。ログイン状態や nonce に依存する応答が含まれます - プレビュー(
preview=true)やカスタマイザー - WooCommerce などのカート・チェックアウト・マイアカウント、
wc-ajaxのリクエスト - GET 以外のリクエスト(POST のコメント送信やフォーム送信)
- 別ドメインのリソース(広告・解析タグ・外部フォント)。必要な場合だけ個別に検討します
もう1つ注意したいのがログインユーザーの HTML です。管理バーや nonce が埋め込まれたページをキャッシュすると、ログアウト後もその HTML が表示されることがあります。Service Worker からはログイン Cookie を読めないため、サーバー側で「保存しないでほしい応答」に印を付け、Service Worker がそれを見て保存を見送る方法が確実性の高いやり方です。
実装例1:ログイン中は保存不可の印を付ける(PHP)
ログイン中のフロントページに Cache-Control の no-store などを付けます。WordPress コアの nocache_headers() を使えば、適切なヘッダー一式を送れます。コアのバージョンによってはログイン中に同様のヘッダーを既に送っている場合もありますが、明示しておくと Service Worker 側の判定が安定します。コードは子テーマの functions.php か mu-plugin に置きます。
<?php
// wp-content/mu-plugins/wpdr-pwa.php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// ログイン中のフロントページはキャッシュさせない
add_action( 'template_redirect', function () {
if ( is_user_logged_in() ) {
nocache_headers();
}
}, 1 );
// Service Worker を登録する小さなスクリプトを出力
add_action( 'wp_enqueue_scripts', function () {
wp_register_script( 'wpdr-sw-register', false, array(), null, array( 'in_footer' => true ) );
wp_enqueue_script( 'wpdr-sw-register' );
wp_add_inline_script(
'wpdr-sw-register',
sprintf(
"if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register(%s, { scope: %s }); }); }",
wp_json_encode( home_url( '/sw.js' ) ),
wp_json_encode( home_url( '/' ) )
)
);
} );
Service Worker が制御できる範囲(スコープ)は、原則として sw.js を置いたディレクトリ以下です。サイト全体を対象にするには、sw.js をサイトのトップ(home_url('/') に対応するディレクトリ。通常は WordPress の index.php がある場所)に置きます。Apache の WordPress 用 .htaccess は実在するファイルをそのまま配信するため、追加の書き換えルールは不要です。また Service Worker は HTTPS(または localhost)でしか動きません。
実装例2:リソース別に戦略を割り当てる(sw.js)
ライブラリを使わずに書いた例です。動きを理解したうえで、規模が大きくなったら Workbox に置き換えるのも良い選択です。
// サイトのトップに置く sw.js
const VERSION = 'v1';
const STATIC_CACHE = `static-${VERSION}`;
const PAGE_CACHE = `pages-${VERSION}`;
const IMAGE_CACHE = `images-${VERSION}`;
const OFFLINE_URL = new URL('offline/', self.registration.scope).href;
const MAX_IMAGES = 60;
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(PAGE_CACHE).then((cache) => cache.add(OFFLINE_URL))
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keep = [STATIC_CACHE, PAGE_CACHE, IMAGE_CACHE];
const keys = await caches.keys();
await Promise.all(keys.filter((k) => !keep.includes(k)).map((k) => caches.delete(k)));
await self.clients.claim();
})());
});
function shouldBypass(request, url) {
return request.method !== 'GET'
|| url.origin !== self.location.origin
|| url.pathname.includes('/wp-admin/')
|| url.pathname.includes('/wp-login.php')
|| url.pathname.includes('/wp-json/')
|| url.searchParams.has('rest_route')
|| url.searchParams.has('preview')
|| url.searchParams.has('customize_changeset_uuid')
|| url.searchParams.has('wc-ajax')
|| /\/(cart|checkout|my-account)\//.test(url.pathname);
}
function isStorable(response) {
const cc = response.headers.get('Cache-Control') || '';
return response.ok && !/no-store|private/i.test(cc);
}
async function trimCache(cacheName, max) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
for (let i = 0; i < keys.length - max; i++) {
await cache.delete(keys[i]);
}
}
async function networkFirst(request) {
const cache = await caches.open(PAGE_CACHE);
try {
const response = await fetch(request);
if (isStorable(response)) {
await cache.put(request, response.clone());
}
return response;
} catch (error) {
return (await cache.match(request)) || (await cache.match(OFFLINE_URL)) || Response.error();
}
}
async function cacheFirst(request) {
const cache = await caches.open(STATIC_CACHE);
const cached = await cache.match(request);
if (cached) return cached;
const response = await fetch(request);
if (isStorable(response)) {
await cache.put(request, response.clone());
}
return response;
}
async function staleWhileRevalidate(event) {
const cache = await caches.open(IMAGE_CACHE);
const cached = await cache.match(event.request);
const update = fetch(event.request).then(async (response) => {
if (isStorable(response)) {
await cache.put(event.request, response.clone());
await trimCache(IMAGE_CACHE, MAX_IMAGES);
}
return response;
}).catch(() => undefined);
if (cached) {
event.waitUntil(update);
return cached;
}
return (await update) || Response.error();
}
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (shouldBypass(request, url)) return; // 何もしなければ通常のネットワーク通信になる
if (request.mode === 'navigate') {
event.respondWith(networkFirst(request));
} else if (/\.(?:css|js|woff2?)$/.test(url.pathname)) {
event.respondWith(cacheFirst(request));
} else if (request.destination === 'image') {
event.respondWith(staleWhileRevalidate(event));
}
});
オフライン時に表示する固定ページ(例: スラッグ offline)を先に作っておいてください。このページが存在しないとインストール処理が失敗し、Service Worker が有効になりません。画像キャッシュは件数で上限を設けています。上限が無いとブラウザの保存容量を使い続け、容量不足のときにまとめて削除される原因になります。
戦略選びでつまずきやすい点
Cache First で困るのは「URL が同じまま中身が変わるファイル」です。子テーマの style.css を wp_enqueue_style() で読み込む際にバージョンを固定値にしていると、CSS を直しても反映されません。バージョンに filemtime() の値を渡すか、更新のたびに番号を上げてください。メディアライブラリで同じファイル名の画像を差し替えた場合も同様に古い画像が残るため、画像は Stale-While-Revalidate にしておくと次回の表示で入れ替わります。
ページキャッシュや CDN との二重キャッシュにも注意が必要です。Network First の「ネットワーク」は CDN やキャッシュプラグインの応答であることが多く、そちらが古ければ Service Worker が新しくしても古いままです。記事更新時のパージ設定は、Service Worker とは別に確認してください。
更新・確認・解除の方法
sw.js を変更したら VERSION を上げます。ブラウザは sw.js の内容がバイト単位で変わると新しい版をインストールし、activate で古いキャッシュを削除します。ブラウザは Service Worker 本体の更新確認では HTTP キャッシュを基本的に使いませんが、CDN が sw.js を長期間キャッシュしていると更新が届かないため、sw.js だけはキャッシュ対象から外しておくと安心です。
確認には Chrome の開発者ツールの「Application」パネルが便利です(2026年10月現在の表記。バージョンにより変わることがあります)。Service Workers の項目で登録状態とスコープを、Cache Storage の項目で保存された URL を確認できます。オフラインにする設定を使えば、オフラインページへの切り替えも試せます。ログインした状態で数ページ巡回し、PAGE_CACHE に HTML が保存されていないことも確かめてください。
不具合が出たときに備えて、解除用の sw.js も用意しておきます。同じ場所の sw.js を次の内容に置き換えると、訪問者のブラウザで次回の更新確認時にキャッシュが削除され、登録も解除されます。
// 緊急時に差し替える解除用 sw.js
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const keys = await caches.keys();
await Promise.all(keys.map((k) => caches.delete(k)));
await self.registration.unregister();
})());
});
sw.js ファイルを削除するだけでは、すでに登録されたブラウザでは古い Service Worker が動き続けることがあります。解除用ファイルへの差し替えを手順として覚えておいてください。
プラグインを使う場合
自作せずに導入したい場合、WordPress.org には「PWA」プラグインがあり、Service Worker の登録やキャッシュ経路の設定を PHP から行う仕組みを提供しています。ただし開発状況や提供される関数・設定項目は時期によって変わるため、導入前に公式ページで最終更新日と対応バージョンを確認してください。キャッシュ系プラグインやセキュリティ系プラグインとの組み合わせによっては、Service Worker の配信や登録が妨げられることもあります。どちらの方法でも、ステージング環境で試し、本番反映前にバックアップを取ってください。
まとめ
- 戦略は「古い内容を見せて困るか」「URL が同じまま中身が変わるか」で選ぶ
- HTML は Network First、
?ver=付きの CSS・JS は Cache First、画像は Stale-While-Revalidate が基本 - 管理画面・ログイン・REST API・プレビュー・カート・POST はキャッシュしない
- ログイン中は
nocache_headers()で印を付け、Service Worker は no-store / private の応答を保存しない - sw.js はサイトのトップに置き、変更時は
VERSIONを上げる。解除用 sw.js を用意しておく - CDN やページキャッシュとの二重キャッシュも含めて、ステージングで確かめる