コンテンツにスキップ

キャッシュ戦略まとめ

本ドキュメントは、Storefront / Vendure / WordPress 間のキャッシュ方針を整理したものです。

概要

  • Storefront (Vite SPA): public GET の edge cache opt-in と revalidation metadata を管理
  • Vendure: Redis があれば Redis キャッシュ、なければ in-memory
  • WordPress (WPGraphQL): Storefront/Vendure の二層で TTL キャッシュを管理

現在の実装ステータス

現時点では、production 系のキャッシュ戦略実装は完了している。追加方針として、 local / mock では shared cache を明示的に無効化し、さらに 会員制 catalog と公開 editorial を route 単位で分離する。

  • Storefront
  • 公開 GET の edge cache opt-in / private API の no-store 制御を実装済み
  • local / mock では shared cache bypass を優先し、revalidate 指定や公開 GET でも no-store を返す
  • page route では /articles/*/announcements/* だけを認証非依存の公開例外とし、//products/*/campaigns/* など commerce 導線はログイン前提のため edge cache 対象へ広げない
  • 実装箇所:
    • apps/storefront/src/lib/http/cache-control.ts
    • apps/storefront/src/worker-api.ts
    • apps/storefront/src/lib/http/cache-control.ts
  • Vendure
  • WordPress 連携にプロセス内 TTL キャッシュ / inflight dedupe / stale-if-error を実装済み
  • 重要 CMS (settings / menus / banners / rankings) には last-known-good snapshot を shared cache へ保存する実装を追加済み
  • 実装箇所:
    • packages/plugins/src/cms-integration/wordpress/wordpress.service.ts
    • apps/vendure-server/src/redis-strategy.ts
  • インフラ
  • production は ritsubi-redis-prod
  • staging は ritsubi-redis-staging
  • いずれも Fly private network 経由で *.internal + REDIS_TLS=false の dedicated Redis app を使う

未完了なのは 運用上の追従作業 だけで、主に以下を Issue で追跡している。

  • #453: staging の AWS Secrets Manager 正本追従
  • #455: production の AWS Secrets Manager 正本追従と旧 ritsubi-redis-prd 整理

Redis と Cloudflare の責務分離

同じ「キャッシュ」でも、本リポジトリでは RedisCloudflare を別用途で使う。

主用途 向いているもの 向いていないもの
Redis / Vendure shared cache アプリ内部の共有状態・再起動後も残したい補助データ session、Vendure shared cache、WordPress last-known-good snapshot、複数インスタンスで共有したい値 ブラウザ向け静的配信、公開ページのエッジ配信
Cloudflare edge cache 外向き HTTP 配信の高速化 /assets/*、公開アセット、明示的に opt-in した公開 GET レスポンス 認証依存レスポンス、POST API、内部 GraphQL fetch、session 共有

原則:

  • Redis は origin / app 側の共有キャッシュ
  • Cloudflare は edge / 配信キャッシュ
  • WordPress を正本とする CMS データのうち、障害時に最後の正常値を返したいものは Redis 側で保持する
  • Cloudflare 側は 公開 GET にだけ明示的に opt-in し、private / auth 系 API は no-store を返す
  • Storefront の page route では /articles/*/announcements/* 以外を公開面とみなさず、商品・販促・顧客導線の HTML/JSON を edge cache へ載せない
  • React Dashboard / admin API の asset preview は Cloudflare edge cache に載せず、 Vendure Server 同居 /assets/ で read-after-write の即時性を優先する
  • HTML / JSON を Cloudflare で積極的にキャッシュする場合は、レスポンスヘッダーに加えて Cloudflare Cache Rules の対象 URL を明示的に絞る

Storefront の環境別キャッシュマトリクス

production / staging / preview local / mock 補足
公開 GET の edge cache 明示 opt-in した route のみ有効 no-store /api/campaigns など認証非依存レスポンスだけを対象にする
shared fetch metadata (revalidation) 個別 TTL を付与してよい 共通 helper が cache: "no-store" を優先 resolveStorefrontSharedFetchOptions() を経由する
Vite 静的 asset 配信 /assets/* を長期キャッシュ可 /assets/* を配信 worker.js + ASSETS binding が静的配信を担う
TanStack Query 通常の staleTime を維持 staleTime を短くし、invalidateQueries で最新表示を優先 queryKey と invalidate 方針を app 側で統一する
private / auth 依存 API no-store no-store /commerce/shop-api、顧客系 API、health、revalidate など

追加ルール:

  • local / mock の cache bypass は画面ごとに実装せず、Storefront 共通 helper に集約する。
  • 新規の public cache は、Cookie / Authorization 非依存を証明できる導線だけに限定する。
  • 認証不要な page route の既定値は増やさず、公開例外は /articles/*/announcements/* の editorial 導線に限る。
  • PUBLIC_VENDURE_FETCH_OPTIONS を使う fetch は Cookie が送信されうるため、truly-public な取得経路を別途用意するまでは cross-request cache を追加しない。

Storefront の取得キャッシュ

Storefront 表示データの即時無効化

Vendure / WordPress 側の更新を Storefront 表示へ反映する入口は domain 単位の revalidation API に集約する。

  • 共通入口: POST /api/revalidate/storefront-data
  • header: x-cms-revalidate-secret: <CMS_REVALIDATE_SECRET>
  • body: {"domains":["catalog"],"source":"vendure","reason":"product.updated"}
  • revision 確認入口: GET /api/storefront-data-revisions?domains=catalog
  • domain は catalog / cms / checkout / customer / config
  • catalog 更新では Storefront shared cache の storefront-data:catalog tag version を bump し、browser/session cache key にも revision を含める。
  • Vendure 側は StorefrontDataInvalidationService が商品・variant・collection 更新イベントを拾い、catalog domain を revalidate する。
  • collection filter 更新は materialized contents 完了後の CollectionModificationEvent を正本にして revalidate し、古い membership を 再取得させない。
  • TTL は webhook / revalidate 失敗時の安全網として残す。通常反映は revision bump によって次回 navigation/refetch で最新化する。

CMS コンテンツ連携のキャッシュ設定

以下の TTL は production / staging / preview で適用する。local / mock では共通 helper が no-store を優先し、shared cache を使わない。

なお、WordPress revalidate webhook での即時反映を保つため、browser の sessionStorage には CMS fetch 結果や site shell model を保持しない。 public CMS の TTL は server / edge の shared cache にだけ適用し、同一ブラウザ session で footer / header が stale のまま残る状態を避ける。

種別 取得元 キャッシュ 設定箇所
ストア設定(OGP含む) WordPress 24時間 (revalidate: 86400) apps/storefront/src/lib/cms/content/store-settings.ts
メニュー WordPress 1時間 (revalidate: 3600) apps/storefront/src/lib/cms/content/menus.ts
お知らせ一覧 WordPress 1時間 (revalidate: 3600) apps/storefront/src/lib/cms/content/announcements.ts
お知らせ詳細 WordPress 5分 (revalidate: 300) apps/storefront/src/lib/cms/content/announcements.ts
キャンペーン一覧 WordPress 1時間 (revalidate: 3600) apps/storefront/src/lib/cms/content/campaigns.ts
キャンペーン詳細 WordPress 5分 (revalidate: 300) apps/storefront/src/lib/cms/content/campaigns.ts
メインバナー(ホームヒーロー) WordPress 1時間 (revalidate: 3600) apps/storefront/src/lib/cms/content/hero-slides.ts
商品リッチ説明 WordPress 6時間 (revalidate: 21600) apps/storefront/src/lib/cms/content/products.ts
固定ページ WordPress 5分 (revalidate: 300) apps/storefront/src/lib/cms/content/pages.ts

OGP 反映

  • OGP は WordPress のストア設定を参照して metadata helper で反映する
  • 実質のキャッシュは ストア設定の revalidate: 86400 に依存
  • 設定箇所: apps/storefront/src/lib/storefront-metadata.ts

Cloudflare edge cache の適用方針

  • Cloudflare は 公開 GET の配信最適化に限定する
  • local / mock では app 側が no-store を返し、edge cache opt-in を無効化する
  • 現時点で app 側から edge cache を許可する主対象は、公開 CMS 一覧 API のような 認証非依存レスポンス
  • campaigns は CMS 由来の本文を持っていても storefront では commerce 導線として扱うため、page route の public cache 対象へ広げない
  • shop-api、顧客階層、CMS revalidate、health check などの 内部 / private APICache-Control: no-store を明示する
  • Cloudflare へは Cloudflare-CDN-Cache-Control / CDN-Cache-Control を返し、ブラウザ向け Cache-Control と分離する
  • 公開アセット配信は Cloudflare 側の得意領域なので、wp-assets.<domain> のようなアセット専用ホストで長めにキャッシュする
  • ただし Vendure asset のうち admin API が返す URL は ec-assets* ではなく same-server /assets/ を正本とし、Dashboard 画像アップロード直後の stale 404 を避ける

Storefront static asset delivery(Cloudflare Workers + ASSETS)

  • 静的 asset は Vite build の /assets/*public/ 配下のトップレベルファイルを正本にする。
  • Cloudflare Worker (apps/storefront/worker.js) は /assets/*/favicon.ico/manifest.json などのファイル要求を ASSETS binding へそのまま流す。
  • それ以外の navigation request は / へ fallback し、TanStack Router が画面遷移を処理する。
  • shared cache の opt-in は HTML 全体ではなく、認証非依存 API と fetch helper の metadata に限定する。
  • 詳細手順は storefront-cloudflare-deploy.md を参照する。

Storefront で header 制御を実装済みの route

route 方針 補足
/api/campaigns public edge cache opt-in production 系のみ public。local / mock では no-store
/api/customer-hierarchy private, no-store Cookie / Authorization 依存
/api/health/live no-store Tier 0 Safe Probe(連続ポーリング可)
/api/health/ready no-store Tier 0 Safe Probe(連続ポーリング可)
/api/version no-store Tier 0 Safe Probe(デプロイ検証)
/api/health/cms no-store 診断専用。Vendure / WordPress への upstream fan-out を含むため連続ポーリング不可
/api/revalidate/cms no-store invalidation endpoint
/commerce/shop-api private, no-store GraphQL proxy / 認証依存

/api/health/live, /api/health/ready, /api/version は Safe Probe として連続ポーリング・外形監視に使用可。/api/health/cms は CMS 配信経路の診断専用で upstream への fan-out があるため、外形監視の対象にしてはならない。詳細は 監視・運用設計書 — Storefront ヘルスチェックエンドポイント を参照。

Vendure のキャッシュ

  • Redis が利用可能な場合は Redis を使う(Vendure 全体のキャッシュ層)
  • WordPress 連携 (WordPressService) はプロセス内 TTL キャッシュを利用
  • タイムアウト: WORDPRESS_FETCH_TIMEOUT_MS(既定 5000ms)
  • TTL: WORDPRESS_FETCH_CACHE_TTL_SECONDS(既定 300秒)
  • 上限件数: WORDPRESS_FETCH_CACHE_MAX_ENTRIES(既定 500)
  • 同一キーの同時リクエストは inflight Promise を共有
  • 再取得失敗時は stale-if-error で古い値を返却
  • settings / menus / banners / rankings は last-known-good snapshot も共有キャッシュへ保存する
  • snapshot TTL: WORDPRESS_SNAPSHOT_TTL_SECONDS(既定 86400秒)
  • Redis が有効ならプロセス再起動後も snapshot を再利用できる
  • 設定箇所: packages/plugins/src/cms-integration/wordpress/wordpress.service.ts

Redis を使う理由

  • Vendure -> WordPress の内部 fetch は Cloudflare edge cache の対象にしづらい
  • session や snapshot は 複数インスタンス間で共有したい
  • Storefront Worker の再デプロイ / Vendure 再起動後も値を残したい
  • そのため、CMS の last-known-good は Cloudflare KV/D1 ではなく Vendure shared cache (Redis) に寄せる

現在の Redis 利用先

  • ritsubi-ecommerceritsubi-redis-prod.internal
  • ritsubi-ecommerce-stagingritsubi-redis-staging.internal
  • staging も production と同じ Redis 経路を通すため、キャッシュ挙動の検証先として利用できる

WordPress (WPGraphQL)

  • 連携は Vendure の WordPress プラグイン経由
  • 画像URLは VITE_PUBLIC_WORDPRESS_ASSET_BASE_URL を優先して正規化する(/wp-content/... を Cloudflare 配下のアセットドメインに寄せる)
  • 設定箇所:
  • apps/storefront/src/lib/cms/content/url.ts
  • apps/storefront/src/lib/cms/content/content-processor.ts

メモリ消費の最適化 (Memory Optimization)

Cloudflare Workers (Edge Runtime) の厳しいメモリ制限 (128MB) と、長時間セッションによるブラウザのメモリ肥大化を防止するための設定です。

Vite 静的 asset キャッシュ (Client-side)

ブラウザ側ではハッシュ付き /assets/* を配信し、キャッシュ破棄は build ごとの fingerprint で吸収します。

  • 設定: apps/storefront/vite.config.ts
  • 配信: apps/storefront/worker.js + apps/storefront/wrangler.toml

TanStack Query キャッシュ方針 (Client-side)

GraphQL 取得結果は queryKey ごとに管理し、local / mock では stale を短く、更新後は invalidateQueries を正本にして再取得する。

  • 設定: packages/sdk/shop/src/query-client.ts
  • 既定値:
  • staleTime: 30_000
  • gcTime: 5 * 60_000
  • refetchOnWindowFocus: false
  • mutation 後は画面ごとの queryKey を invalidate する

route loader / request-time バルクデータ取得の制限

route loader や request-time fetch で大量のデータを一括でメモリへ読み込む処理を制限し、Worker のメモリ超過を防止します。

  • CMS コンテンツ取得: apps/storefront/src/lib/cms/content/products.ts において、インデックス取得の maxPages3 (最大 300件) に制限。

Sentry サンプリング

パフォーマンス監視データのバッファリングによるメモリ消費を抑制します。

  • 設定: tracesSampleRate: 0.1 (Production)

即時反映(Webhook + tag invalidation)

  • Storefront に POST /api/revalidate/cms を実装済み
  • 認証:
  • ヘッダー x-cms-revalidate-secret または body secret
  • CMS_REVALIDATE_SECRET と照合
  • payload:
  • event: announcement.updated | campaign.updated | page.updated | menu.updated | settings.updated | all
  • tags?: string[](指定時は event より優先)
  • event→tag:
  • announcement.* => ["cms","announcements"]
  • campaign.* => ["cms","campaigns"]
  • page.* => ["cms","pages"]
  • menu.* => ["cms","menus"]
  • settings.* => ["cms","settings","banners"]
  • all => ["cms","announcements","campaigns","pages","menus","settings","banners","products","rankings"]

[!IMPORTANT] POST /api/revalidate/cms が無効化するのは Storefront の fetch cache であり、Vendure の CmsIntegrationPlugin が持つ WordPress fetch cache までは即時には消えません。menu (headerMenu / footerMenu) は plugin 内でキャッシュを bypass しているため例外で、storefront tag 無効化と同時に最新値が反映されます。一方 product_detail のように Vendure 経由で WPGraphQL を読むその他の導線では、更新後も最大 5 分(DEFAULT_WORDPRESS_FETCH_CACHE_TTL_SECONDS=300)は古い本文や null が返ることがあります。

2026-04 の production 切り分けでは、次の順で確認すると早く収束しました。

  1. direct WP GraphQL (https://cms.../graphql) で記事と styleMode が見えるか
  2. Vendure shop-api で同じ wordPressProductDetail が返るか
  3. Storefront の DOM (product-detail-page-wp-content) に出るか

1 が新しいのに 2 が古い場合は cache expiry を待つか、必要に応じて Vendure runtime を更新してください。