SWRのキャッシュをいい感じにデバッグしたかった
この記事は、Unipos Advent Calendar 2025の19日目の記事です。
Uniposで機能開発を担当しているジミーです。
一応フロントエンドを担当しているので、今回は Chrome 拡張でハマった小ネタ を共有します。
SWR のキャッシュをデバッグしたい!
弊社プロダクトでは API レスポンスの管理に SWR を利用しています。
レスポンスのキャッシュや再検証を自動でやってくれて、とても便利です。
..., it fallbacks to the default cache provider, which is an empty Map.
https://swr.vercel.app/docs/advanced/cache#cache-provider
デフォルトのキャッシュは インメモリ(Map) なので、
ページを更新しない限りどんどん溜まり続けます。
ブラウザのメモリやレスポンスのサイズからすれば大した話ではないかもしれませんが、
知らないところで食いつぶされるメモリには不安になるもので中身を直接デバッグしたくなり SWR DevTools を試してみました。
SWR DevTools を試す
Chrome 拡張として提供されている SWR DevTools をインストールして DevTools を開くと…

実装上の対応が必要かと思いましたが、公式ドキュメントを見る限り SWR v2 以降は特別な対応不要 とのこと。
ただし何度か DevTools を開いたり閉じたりしていると一瞬表示されることはありますが、
少しするとまた動かなくなります。
なぜ動いていないのか
Chrome 拡張を chrome://extensions から見ると、 Service Worker が止まっている ようでした

DevTools のパネルを表示すると Service Worker は一時的に起動しますが、
しばらくするとまた停止してしまいます。
試しに 拡張機能の Debug Console を開いたままにしておく と
Service Worker は停止せず、SWR DevTools も動き続けました。
Chrome 拡張の仕組み
公式ドキュメントにあるアーキテクチャ図はこちら:

公式より:DevTools拡張機能のアーキテクチャ
直感的にはわかりづらいので、SWR DevTools のコードと照らし合わせるとざっくりこんな感じ
[App / SWR]
→ [Inject Script (page context)]
→ [Content Script]
→ [Service Worker]
→ [DevTools Panel]
- DevTools パネルが開いたタイミングで Service Worker が起動
- Content Script が DOM にイベント購読用のスクリプトを注入
- Inject Script が SWR のイベントを購読
- Content Script が window 経由のイベントを chrome.runtime.sendMessage に変換
- Service Worker が受け取って DevTools Panel に転送
実際はポート接続などもう少し複雑ですが、
大まかなイベント伝播はこんな流れです。
ハマったポイント
この仕組みで厄介だったのが、
Service Worker は一定時間メッセージを受け取らないと自動で停止してしまう
という点です。
Service Worker が停止すると DevTools Panel との接続も切れるため、
結果として DevTools が動いていないように見える状態になります。
対策 1: Service Worker の Console を開いたままにする
Service Worker の Debug Console を開いていると、
停止されずに処理が続くため、一時的には DevTools が動作しました。
ただし以下の欠点があります:
- ページをリロードすると接続が切れる
- Inject Script が再挿入されない
- パネルを再度開き直す必要がある
つまり、起動直後のキャッシュ状態が見られない という問題が残ります。
対策 2: <SWRDevTools> を埋め込む
もう一つの方法は、アプリ側で <SWRDevTools> を明示的に埋め込むことです。
これを入れると middleware 的に振る舞うようで、
Inject Script に頼らず DevTools と連携しているようでした。
ドキュメント上では不要と書かれているものの、
Inject Script が安定して挿入されない今回のケースでは有効 でした。
結局コンソールに戻る
起動時からの一覧を確実に見たい という理由から、
今回は結局コンソール出力する方法に落ち着きました。
やってみたログ表示
SWRConfigのproviderを上書きしてキャッシュをログに載せるだけのシンプルなもの。
// createDebugCacheProvider.ts
import type { Cache } from "swr";
type CacheMeta = {
key: string;
lastSetAt: number;
setCount: number;
approxKB?: number;
};
function approxSizeKB(value: unknown): number | undefined {
try {
const json = JSON.stringify(value);
// UTF-16 想定で 2byte
// 多めに見積もり
return Math.round((json.length * 2) / 1024);
} catch {
return undefined;
}
}
export function createDebugCacheProvider() {
const store = new Map<string, any>();
const meta = new Map<string, CacheMeta>();
const provider: Cache = {
get(key: any) {
return store.get(String(key));
},
set(key: any, value: any) {
const k = String(key);
store.set(k, value);
const m =
meta.get(k) ?? {
key: k,
lastSetAt: 0,
setCount: 0,
};
m.lastSetAt = Date.now();
m.setCount += 1;
m.approxKB = approxSizeKB(value);
meta.set(k, m);
return provider;
},
delete(key: any) {
store.delete(String(key));
meta.delete(String(key));
return true;
},
keys() {
return store.keys();
},
};
function print() {
const rows = [...meta.values()]
.sort((a, b) => b.lastSetAt - a.lastSetAt)
.map((m) => ({
key: m.key,
lastSetAt: new Date(m.lastSetAt).toLocaleTimeString(),
setCount: m.setCount,
sizeKB: m.approxKB ?? "-",
}));
console.table(rows);
}
function clearAll() {
store.clear();
meta.clear();
console.info("[SWR] cache cleared");
}
return {
provider: () => provider,
print,
clearAll,
};
}
// pages/_app.tsx
import type { AppProps } from "next/app";
import { SWRConfig } from "swr";
import { createDebugCacheProvider } from "../lib/createDebugCacheProvider";
export default function App({ Component, pageProps }: AppProps) {
const isDev = process.env.NODE_ENV === "development";
const debug = isDev ? createDebugCacheProvider() : null;
if (isDev && typeof window !== "undefined") {
// Console から叩けるように公開
(window as any).__SWR_CACHE__ = {
print: debug?.print,
clearAll: debug?.clearAll,
};
}
return (
<SWRConfig value={debug ? { provider: debug.provider } : undefined}>
...
</SWRConfig>
);
}
コンソールログでの出力
まあ、今のところはこれで十分そうだな
ソートはできてもいいかも
まとめ
拡張機能は便利な反面、知らない前提があると結構ハマってしまうのだなという経験でした
最終リリースから2年以上空いているようなので今後貢献できそうであれば参加していきたい
おまけ
ManifestV2とMV3
Chrome 拡張は Manifest V3 への移行で、Service Worker ベースになったとのことです。
その結果、バックグラウンドで永続的に待ち受ける処理ができなくなっています。
これはパフォーマンスやセキュリティ面では理にかなってそうですが。
React DevToolsはどうなってるのか
同様の拡張機能で安定しているReact DevToolsについてもさらっと
[ React Renderer ]
│
│ commit / update
▼
[ injected script ]
( __REACT_DEVTOOLS_GLOBAL_HOOK__ )
│
│ bridge
▼
[ DevTools Panel ]
React は REACT_DEVTOOLS_GLOBAL_HOOK というグローバルフックを利用した仕組み を持っているため、Service Worker を介さなくても DevTools と連携できるとのこと。
ServiceWorkerいらないことあるんだ
Discussion