1. なぜ ChatGPT App でキャッシュとエッジを考えるのか
クラシックなウェブアプリでも速度は重要ですが、そこではユーザーがスピナーを目にできます。ChatGPT App はもう少し複雑です。ユーザーがモデルと会話し、モデルがあなたの App を呼ぶことがあります。ウィジェットはポップアップし、できるだけ早く有用な情報を表示する必要があります。
実務はかなり明確です: latency = コスト。応答が遅いほど離脱の確率が上がり、余計な LLM/バックエンド呼び出しはモデルやインフラの直接コストになります。キャッシュはその両方を減らします。
ChatGPT Apps 特有の事情:
- ChatGPT からあなたの App へのリクエストはネットワークと複数のレイヤーを経由します。各ステップのミリ秒が積み重なります。
- MCP/HTTP エンドポイントには実際のタイムアウトがあります(Vercel の serverless 関数や edge 関数にも)。間に合わなければ、ChatGPT はエラーを見て、ときに「幻覚」的な応答を返すことさえあります。
- GiftGenius の多くのデータは毎秒変わるわけではありません。ギフトのカテゴリ構造、さまざまなセグメント向けの「トップアイデア」のまとめ、機能設定などです。毎回 DB や外部 API を叩くのは非効率です。
ここで役立つのが次の要素です:
- CDN とエッジキャッシュにより、静的ファイルやキャッシュ可能な JSON を高速配信。
- HTTP キャッシュ(Cache-Control/ETag/SWR)により、再リクエストを速く安く。
- Vercel のエッジ関数は、ChatGPT やユーザーにできるだけ近い場所で軽量ロジックを実行するためのもの。これは「ミニ・バックエンド」ではありません。
2. GiftGenius におけるレイテンシの内訳とキャッシュポイント
まず、レイテンシがどこで生まれているのかを正直に図示すると役立ちます。
sequenceDiagram
participant User as ユーザー
participant ChatGPT as ChatGPT
participant App as ChatGPT App (Apps SDK)
participant GW as MCP Gateway / Edge
participant GiftAPI as Gift REST API / ギフト用マイクロサービス
participant DB as カタログ/データベース
User->>ChatGPT: "兄へのプレゼントを選んで"
ChatGPT->>App: ツール呼び出し + ウィジェットのレンダリング
App->>GW: HTTP / MCP リクエスト(カテゴリ、リスト)
GW->>GiftAPI: HTTP (REST)
GiftAPI->>DB: カタログ/レコメンドのクエリ
DB-->>GiftAPI: 応答
GiftAPI-->>GW: 応答 (JSON)
GW-->>App: 応答 (JSON)
App-->>ChatGPT: 結果を含むウィジェット
ChatGPT-->>User: メッセージ + UI
どこで「ショートカット」できるか?
- ChatGPT とあなたの境界の間 — CDN/エッジキャッシュ(Vercel CDN/Edge Network)。ウィジェットの不変アセットやキャッシュ可能な JSON を origin サーバーに到達させずに配信できます。
- Gateway と内部の REST/HTTP サービス(Gift REST API、Commerce REST API など)や DB の間 — アプリケーションキャッシュ(Redis/インメモリ/DB キャッシュ)。たとえば「ギフトカテゴリ一覧」のような同一リクエストを何度も流さないためです。
本講義では、ChatGPT と Vercel に近い HTTP/エッジ層に焦点を当てます。
3. アーキテクチャにおけるキャッシュの種類
「レイヤーケーキ」なアーキテクチャなら、キャッシュも複数種類になります。
| キャッシュの種類 | 配置 | 用途 |
|---|---|---|
| ブラウザキャッシュ | ChatGPT クライアント内(ブラウザ/デスクトップ) | ウィジェットの静的ファイル、アイコン、フォント(制御は限定的) |
| CDN / エッジキャッシュ | Vercel/Cloudflare のエッジノード上 | 静的ファイル + 共有 JSON(カテゴリ、設定、共通のまとめ) |
| アプリケーションキャッシュ | MCP Gateway やバックエンドサービス内(Redis、インメモリ) | DB/外部 API への重いクエリの結果 |
| DB キャッシュ/マテリアライズ | DB 内(マテリアライズドビュー等) | 事前計算済みの集計、分析 |
ここからは最初の 2 つ、すなわち HTTP キャッシュ + CDN/エッジに集中します。
4. HTTP キャッシュ: Cache-Control、max-age と s-maxage
HTTP キャッシュは主に Cache-Control ヘッダーで制御します。ブラウザ/ChatGPT クライアントや CDN が応答をキャッシュできるか、どれくらい保持できるかが決まります。
主要なディレクティブ:
- max-age — ブラウザが応答をキャッシュできる秒数。
- s-maxage — 共有キャッシュ(CDN/プロキシ)がキャッシュできる秒数。
- public — 共有キャッシュに保存可能。
- private — 特定クライアント専用。CDN はキャッシュしない。
GiftGenius の例:
- ウィジェットの JS/CSS/フォント — 版管理されたファイル(名前にハッシュあり)は安心して Cache-Control: max-age=31536000, immutable で配信できます。
- ギフトカテゴリ一覧の JSON — 全ユーザー共通なので、 public, s-maxage=60(あるいはそれ以上)が妥当です。
GET /api/gifts/categories を CDN で 60 秒キャッシュする、最も簡単な Next.js のルートハンドラー:
// app/api/gifts/categories/route.ts
import { NextResponse } from "next/server";
export const runtime = "nodejs"; // 通常の serverless 関数
export async function GET() {
// ここで DB/外部 API にアクセスする想定
const categories = [
{ id: "for_brother", title: "兄へのプレゼント" },
{ id: "for_mom", title: "母へのプレゼント" },
];
return NextResponse.json(categories, {
headers: {
// CDN に 60 秒のキャッシュを許可
"Cache-Control": "public, s-maxage=60",
},
});
}
Vercel CDN は応答を 60 秒保持し、その間の ChatGPT からの JSON リクエストはあなたの関数に到達しません。高速かつ低コストです。
5. ETag: コンテンツのフィンガープリントと 304 Not Modified
ETag はリソースの「指紋」(通常は内容のハッシュ)です。動作の流れは次のとおりです。
- サーバーは ETag: "v1-abc123" ヘッダー付きで応答します。
- 次回、クライアントは If-None-Match: "v1-abc123" を送信します。
- コンテンツが変わっていないとサーバーが判断したら、本文なしで 304 Not Modified を返します。
重要: ETag はトラフィックを節約しますが、必ずしもレイテンシを減らすわけではありません。サーバーまでの往復は必要だからです。ChatGPT Apps の文脈では、大きな JSON 応答で有用ですが、ETag だけで劇的な速度向上を期待すべきではありません。速度には SWR とエッジキャッシュが効きます。
Next.js ハンドラーでのシンプルな ETag の例(暗号学的なハッシュは割愛):
// app/api/gifts/config/route.ts
import { NextRequest, NextResponse } from "next/server";
const CONFIG = { version: 1, showExperimentalIdeas: true };
const ETAG = `"v${CONFIG.version}"`;
export async function GET(req: NextRequest) {
const ifNoneMatch = req.headers.get("if-none-match");
if (ifNoneMatch === ETAG) {
// コンテンツは変更されていない — 304 を返す
return new NextResponse(null, { status: 304, headers: { ETag: ETAG } });
}
return NextResponse.json(CONFIG, {
headers: {
ETag: ETAG,
"Cache-Control": "public, s-maxage=300",
},
});
}
実運用では、データのハッシュから ETag を算出するか、DB のレコードバージョンを使うのが一般的です。
6. Stale-While-Revalidate (SWR): 速く、十分に新鮮に
SWR は「古いものをすぐに見せ、新しいものは裏で更新する」アプローチです。次のように実装できます。
- Cache-Control ヘッダーの stale-while-revalidate パラメータで HTTP レベルにて。
- UI レベルで、swr/react-query のようなライブラリを使い、ローカルキャッシュとバックグラウンド refetch を行う。
HTTP ヘッダーでの SWR
典型的なヘッダー:
Cache-Control: public, s-maxage=60, stale-while-revalidate=300
意味:
- 最初の 60 秒は CDN が新鮮なバージョンを返します。
- 61〜360 秒は、CDN は古い応答を即時返却しつつ、 バックグラウンドで origin に新バージョンを取りに行けます。
- 360 秒を過ぎると新しいコンテンツ取得はブロッキングになります。
ユーザー(と ChatGPT)はピーク時でも即時の応答を受け取り、裏でキャッシュが穏やかに更新されます。GiftGenius では、たとえば「年末向けトップギフトまとめ」のように毎秒は変わらないものに最適です。
例:
// app/api/gifts/top/route.ts
import { NextResponse } from "next/server";
export async function GET() {
const topGifts = [
{ id: "coffee_mug", title: "文字入りマグカップ" },
{ id: "smart_led", title: "スマート電球" },
];
return NextResponse.json(topGifts, {
headers: {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=300",
},
});
}
UI ウィジェットでの SWR(React)
GiftGenius のウィジェットは ChatGPT のサンドボックス内で動き、任意の React コードを使えます。すでに window.fetch で API を叩けます。 ここに swr を加えて、ウィジェット側のキャッシュを組み立てます。
// widget/GiftTopList.tsx
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
export function GiftTopList() {
const { data, isLoading } = useSWR(
"https://api.giftgenius.com/api/gifts/top",
fetcher,
{ revalidateOnFocus: false } // チャットではフォーカスの挙動が特殊なので無効化
);
if (isLoading && !data) return <div>アイデアを読み込み中…</div>;
return (
<ul>
{data?.map((gift: any) => (
<li key={gift.id}>{gift.title}</li>
))}
</ul>
);
}
動作:
- 初回レンダーで API へリクエストします。
- 結果はウィジェット内の swr キャッシュに保存されます。
- 再レンダー時(または ChatGPT が同じキーでこのウィジェットを再挿入したとき)にはキャッシュから読み出します。 ユーザーはチラつきやスピナーを見ずに済み、裏で更新が走ることがあります。
このように、SWR を二重化します。
- CDN/HTTP レベル — origin を守るため。
- UI レベル — ユーザー体験を守るため。
すべてをまとめると:
- シンプルな Cache-Control(max-age/s-maxage) — 基本レイヤー。CDN とクライアントにキャッシュ権を与え、負荷を下げる。
- ETag + If-None-Match — 大きな JSON でトラフィック節約が重要なときに追加。ただしネットワーク往復は残る。
- stale-while-revalidate — わずかに古くても即時配信が重要なとき(カタログ、トップまとめ)に有効。
- UI における SWR(swr/react-query) — ウィジェットの再描画を平滑化し、ChatGPT のサンドボックス内でローカルキャッシュを持つための別レイヤー。
7. GiftGenius では何をどれくらいキャッシュするか
GiftGenius のデータを「キャッシュ可能性の層」に分けて考えます。
CDN/エッジでキャッシュできるもの
全員(あるいは広いセグメント)で共通、かつ頻繁には変わらないもの:
- ウィジェットの静的ファイル: JS/CSS、フォント、アイコン — 「ほぼ永久」(1 年)で immutable。
- ギフトカタログの構造: カテゴリ、セクション、フィルター — 数分〜数時間。
- 共通まとめ(「同僚向けベストアイデア 50$ 以下」など) — 数分〜数十分、ピークシーズンでは特に。
ここでは public、s-maxage に stale-while-revalidate の組み合わせが最適です。
アプリ/Redis でキャッシュしたほうがよいもの
より動的だが、繰り返し発生するデータ:
- 外部 API への重いリクエストの結果(例: 為替レート、外部ストアの最新価格)。
- よくリクエストされるレコメンドのセグメント(性別/年齢/用途など)。
CDN ではトークン/組織/テナントに依存する場合があり適さないことがあります。MCP Gateway や内部 REST サービス側でキャッシュしましょう。完全に自分たちで制御でき、ユーザー間のデータ混在も防げます。
(共有キャッシュでは)キャッシュしてはいけないもの
特定ユーザーに紐づくもの:
- 個別の注文・注文ステータス。
- 支払情報、住所、メール。
- プライベートな注文履歴に基づく個別レコメンド(機微な場合)。
これはアプリ側で厳密なセマンティクスのもとでのみキャッシュ可能(ユーザー間の漏えいは厳禁)で、public な CDN キャッシュでは絶対に不可です。
8. エッジ層: CDN とエッジ関数の違い
似ているようで異なる 2 つを混同しないでください。
- CDN / エッジキャッシュ — 事前計算済みの応答を保存し、ほぼロジックはありません。
- エッジ関数(Vercel Edge / Cloudflare Workers) — エッジノードで実行される小さなコード片。
経験上、Edge ≠ Serverless です。重いビジネスロジックや LLM 呼び出し、BLOB 処理を詰め込みがちですが、タイムアウトや制限で痛い目を見ることになります。エッジ関数は:
- 起動が非常に速い(コールドスタートがほぼゼロ)。
- しかし CPU、実行時間、利用可能な API に強い制限があることが多い(フルの Node.js 不可、長時間ソケット不可など)。
エッジ関数が向いている場合
GiftGenius と ChatGPT App の文脈で、エッジ関数が有用なのは次のような場合です。
- 軽量なルーティング: locale、x-openai-user-location、テナント ID などのヘッダーに応じて、どの地域クラスターへ送るかを決める。
- 簡単なヘッダー付与、フィーチャーフラグ、A/B ルーティング。
- 読み取り専用で高速なエンドポイント(edge-KV や CDN キャッシュから読み、ほぼ計算しない)。
エッジ関数が不向きな場合
- 外部 API への長いリクエスト。
- LLM モデルの呼び出し。
- 複雑なチェックアウトロジック。
- 重いビジネスロジックを伴う MCP ツール。
これらには通常の Next.js serverless 関数(たとえば runtime = "nodejs")や、独立したサービス/クラスターを使いましょう。
Next.js 16 におけるエッジ関数の例
GET /api/geo-router という小さなルートを作り、x-openai-user-location ヘッダー(仮)から、どの地域クラスターにアクセスすべきかを返します。
// app/api/geo-router/route.ts
import { NextRequest, NextResponse } from "next/server";
export const runtime = "edge"; // edge 上で実行
export function GET(req: NextRequest) {
const userLocation = req.headers.get("x-openai-user-location") ?? "US";
const cluster =
userLocation.startsWith("EU") ? "eu-gift-api" : "us-gift-api";
return NextResponse.json({ cluster }, {
headers: {
"Cache-Control": "public, s-maxage=300",
},
});
}
このエンドポイントは:
- 非常に速く動作し(edge)、
- 複雑なことは何もせず、
- CDN でキャッシュできます。
9. GiftGenius の全体アーキテクチャにおけるエッジとキャッシュ
全体像を 1 枚にまとめます。
flowchart TD
ChatGPT[(ChatGPT / ユーザー)]
CDN["CDN / Edge Cache (Vercel)"]
EdgeFn["Edge Functions (ルーティング、フィーチャーフラグ)"]
GW[MCP Gateway]
GiftAPI["Gift REST API Cluster"]
CommerceAPI["Commerce REST API Cluster"]
DB[(DB/External APIs)]
ChatGPT --> CDN
CDN -->|キャッシュヒット| ChatGPT
CDN -->|キャッシュミス| EdgeFn
EdgeFn --> GW
GW --> GiftAPI
GW --> CommerceAPI
GiftAPI --> DB
CommerceAPI --> DB
典型的な流れ:
- ChatGPT ウィジェットが /api/gifts/categories を要求します。
- CDN がキャッシュを確認します。新鮮、または「stale だが許容範囲」のバージョンがあれば、EdgeFn/GW に触れず即返します。
- キャッシュがなければ、(有効なら)EdgeFn 経由、もしくは直接 GW に流れます。
- 必要に応じて GW は内部の Redis キャッシュを使うか、内部 REST サービスや DB へアクセスします。
- 応答は戻され、CDN/エッジキャッシュに入って他のユーザーに配信されます。
この構成により:
- ウィジェットと ChatGPT のレイテンシを下げ、
- MCP Gateway とバックエンドクラスターの負荷を下げ、
- LLM/DB 呼び出しのコスト(重複リクエスト)を削減できます。
10. GiftGenius のための小さな実践断片
カテゴリのキャッシュ + Next.js の revalidate
ここまでは API エンドポイントだけに触れてきましたが、Next.js にはページ自体に対する類似の仕組み(ISR、revalidate)もあります。
revalidate = 60 の server component の例:
// app/(widget)/categories/page.tsx
export const revalidate = 60; // ISR: 60 秒ごとに再生成
async function fetchCategories() {
const res = await fetch("https://api.giftgenius.com/api/gifts/categories");
return res.json();
}
export default async function CategoriesPage() {
const categories = await fetchCategories();
return (
<ul>
{categories.map((c: any) => (
<li key={c.id}>{c.title}</li>
))}
</ul>
);
}
本番では Vercel がこのページの HTML 出力を生成・キャッシュします。これは、ウィジェット/インターフェイスが ChatGPT だけでなく、通常のウェブページ(たとえばデバッグ用パネルやランディング)として開かれる場合にも有用です。
バックエンドサービス内のシンプルなアプリケーションキャッシュ
これはエッジ層ではなく、アプリケーションキャッシュ(Redis/インメモリ。Gift REST API や他のバックエンドサービス内)です。 ただ、最も単純な形を示しておきます。
// Gift REST API 内の疑似コード
const cache = new Map<string, any>();
async function getGiftCategories() {
const key = "gift_categories_v1";
const cached = cache.get(key);
if (cached && Date.now() - cached.ts < 60_000) {
return cached.data; // 60 秒キャッシュ
}
const data = await fetchRealCategories();
cache.set(key, { ts: Date.now(), data });
return data;
}
本番では Map を Redis/Memcached に置き換えるでしょうが、発想は同じです。DB/外部 API へのアクセスを減らします。
すべてを一言でまとめるなら、まず「何をどこで(CDN、エッジ、Redis、DB)キャッシュできるか」を明確にし、そのうえでプラットフォームの「魔法のフラグ」を有効にしましょう。キャッシュは単なる設定ではなくアーキテクチャの一部であり、速度・安定性・コストすべてに影響します。
11. キャッシュとエッジ層でよくある誤り
誤り No.1: 「速くしたいから全部キャッシュする」
典型例: 開発者がすべての JSON 応答に Cache-Control: public, s-maxage=3600 を設定する。数時間後、あるユーザーに他人の注文が見えたり、ChatGPT が在庫情報の古いデータで話し始めたりします。個人向け/機微データには private キャッシュか、CDN キャッシュをオフにしてアプリ側の厳密な分離付きキャッシュにすべきです。
誤り No.2: max-age と s-maxage の取り違え
max-age だけを設定して、CDN も同じだけキャッシュすると期待してしまうケースです。実際には max-age は主にブラウザ向けで、共有キャッシュには s-maxage が必要です。結果としてブラウザはキャッシュするが CDN はしないため、origin は相変わらず過負荷に。「キャッシュを設定したのに」となります。CDN 用には s-maxage を明示しましょう。
誤り No.3: ETag が万能だと思う
ETag はトラフィック節約に優れ、とくに大きな JSON で効果的ですが、ネットワーク往復は残ります。ChatGPT App の世界では、たとえ本文なしの 304 でも、モデルはあなたのサーバー応答を待つ必要があります。レイテンシ短縮が重要なら、エッジキャッシュ + SWR が必要で、ETag は補助的な仕組みです。
誤り No.4: 重いビジネスロジックをエッジ関数に詰め込む
「Vercel Edge から外部 LLM を呼んで、複雑なレコメンドを計算し、外部 API に 3 つも行こう。Edge は速いから!」という発想。しかし実際は、実行時間の制限、完全な Node.js が使えない、謎のエラーといった問題に直面します。エッジは軽いルーティングや A/B に向き、重作業は通常の serverless 関数や独立したバックエンドクラスターに任せるべきです。
誤り No.5: キャッシュの無効化戦略がない
「1 時間キャッシュ」にして高速化に成功。でもビジネスが「価格/カテゴリ/制約を変えたのに、ChatGPT では古いまま?」と言い始めます。そこで手動でキャッシュを掃除し、サービスを再起動…といった泥臭い対応になりがち。重要データには、管理画面からの webhook、バージョン、キー単位など、事前にどうキャッシュをクリアするかを設計しておきましょう。「1 時間後に勝手に新しくなる」頼みは危険です。
誤り No.6: キャッシュとコストの関係を無視する
キャッシュを速度だけの話と捉えがちですが、LLM エコシステムではコストにも直結します。モデルや外部 API への余分な呼び出しはすべてお金です。キャッシュなしだと、MCP サーバーが外部サービス/モデルを過剰に叩いて、月の請求が驚きの額に。適切なキャッシュはレイテンシも請求額も下げます。
誤り No.7: ロケール/地域の混在キャッシュ
GiftGenius は複数国で展開しているのに、キャッシュキーが top_gifts の 1 種類だけ。結果、米国ユーザーにルーブル/ロシアの店舗が出たり、欧州ユーザーにドル/米国の店舗が出たり。キャッシュキー名やルート(例: /api/{locale}/gifts/top)には、locale、currency、tenant を考慮しましょう。
誤り No.8: Next.js/プラットフォームの「魔法」に全面依存
ISR、revalidate、自動 CDN は素晴らしいですが、中身を理解していないと予期せぬ挙動に遭遇します。たとえばページは古い内容だが API は新しい、ChatGPT とブラウザで見えるものが違う、など。Cache-Control、ETag、SWR パターンの仕組みを学び、Next.js は便利なラッパーとして使うのが安全です。
誤り No.9: dev/staging/production で同一のキャッシュ設定
開発環境ではキャッシュがデバッグの邪魔になることが多い(「データを変えたのに、なぜ ChatGPT はまだ古いまとめを見ている?」)。dev ではキャッシュをほぼ無効化(または TTL を数秒)し、production では積極的なキャッシュを有効化する設定を持つとよいでしょう。そうしないと開発が混乱するか、誤ってキャッシュなしの本番をローンチして、MCP Gateway 背後のバックエンドクラスターに嵐のようなリクエストを浴びせることになります。
GO TO FULL VERSION