1. 為什麼在 ChatGPT App 要考慮快取與 edge
在典型的網頁應用中你同樣在乎速度,但至少使用者能看到轉圈的指示器。在 ChatGPT App 中更有意思:使用者在與模型對話,模型有時會決定呼叫你的 App。小工具需要彈出,並且相當快地顯示一些有用的內容。
實務很明確:latency = 金錢。你回應越久,使用者流失的機率越高;多餘的 LLM/後端呼叫意味著模型與基礎設施的直接成本。快取能同時降低這兩者。
ChatGPT Apps 的特性還有:
- 從 ChatGPT 到你的 App 的請求會經過網路與多層中介。每一步的每一毫秒都會累加。
- MCP/HTTP 端點都有真實的逾時(包含 Vercel 的 serverless 函數與 edge 函數)。若你來不及回應,ChatGPT 會看到錯誤,甚至可能開始「幻覺」地編造答案。
- GiftGenius 裡的許多資料不會每秒變動:禮物目錄結構、不同族群的「熱門點子」清單、功能開關設定。每次都重敲資料庫或外部 API 很不划算。
這正是以下機制登場的地方:
- CDN 與 edge 快取,快速提供靜態資源與可快取的 JSON。
- HTTP 快取搭配 Cache-Control/ETag/SWR,讓重複請求更快且更省錢。
- Vercel 的 Edge 函式,在最接近 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/edge 快取(Vercel CDN/Edge Network),可直接提供不可變資產與可快取的 JSON,而不必打到你的 origin 伺服器。
- 在 Gateway 與內部 REST/HTTP 服務(Gift REST API、Commerce REST API 等)及資料庫之間 — 應用快取(Redis/記憶體/資料庫快取),避免同樣的請求(例如「禮物分類清單」)被重複執行一堆次。
本講重點放在 HTTP/edge 層,因為它距離 ChatGPT 與 Vercel 最近。
3. 我們架構中的快取類型
既然是「千層派」式的架構,快取也就有好幾層。
| 快取類型 | 所在位置 | 適用場景 |
|---|---|---|
| 瀏覽器快取 | ChatGPT 用戶端內(瀏覽器/桌面) | 小工具靜態檔、圖示、字型(可控性有限) |
| CDN / edge 快取 | Vercel/Cloudflare 的 edge 節點上 | 靜態資源 + 共用 JSON(分類、設定、通用精選) |
| 應用快取 | 你的 MCP Gateway 或後端服務內(Redis、記憶體) | 對資料庫/外部 API 的重型查詢結果 |
| 資料庫快取/物化 | 在資料庫本身(物化檢視等) | 預先計算的彙總、分析 |
現在聚焦前兩層:HTTP 快取 + CDN/edge。
4. HTTP 快取:Cache-Control、max-age 與 s-maxage
HTTP 快取主要由 Cache-Control 標頭控制。它決定瀏覽器/ChatGPT 用戶端與/或 CDN 能否快取你的回應,以及可以快取多久。
關鍵重點:
- max-age — 瀏覽器可快取回應的秒數。
- s-maxage — shared cache(CDN/代理)可快取的秒數。
- public — 回應可在 shared 快取中被快取。
- private — 回應僅給特定用戶端;CDN 不會快取。
以 GiftGenius 為例:
- 小工具的 JS/CSS/字型是版本化的檔案(檔名含雜湊),可放心以 Cache-Control: max-age=31536000, immutable 提供。
- 禮物分類清單的 JSON 對所有使用者都一樣,適合使用 public, s-maxage=60(或更久)。
最簡單的 Next.js 處理器(Route Handler)為 GET /api/gifts/categories,在 CDN 快取 60 秒:
// 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 與 edge 快取。
簡單的 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。
6. Stale‑While‑Revalidate(SWR):快速且夠新
SWR 的思路是「先立刻顯示舊的,新的在背景更新」。可在以下兩層實作:
- HTTP 標頭 Cache-Control 的 stale-while-revalidate 參數。
- 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 } // 在聊天中 focus 行為較詭異,先關閉
);
if (isLoading && !data) return <div>載入靈感中…</div>;
return (
<ul>
{data?.map((gift: any) => (
<li key={gift.id}>{gift.title}</li>
))}
</ul>
);
}
運作方式:
- 第一次渲染會向我們的 API 發出請求。
- 結果存入小工具內的 swr 快取。
- 之後的重渲染(或新的回合中 ChatGPT 再次插入使用相同 key 的小工具)會直接從快取讀取。 使用者不會看到「閃爍」與轉圈,背景則可進行更新。
因此,我們同時結合兩層 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/edge 層快取
這些對所有人(或廣大分眾)相同、且不常變動:
- 小工具靜態資源:JS/CSS、字型、圖示 — 幾乎「永久」(一年),搭配 immutable。
- 禮物目錄結構:分類、區段、過濾器 — 幾分鐘到幾小時。
- 通用精選(例如「50 美元以內的同事好禮」)— 幾分鐘到十幾分鐘,旺季尤其適合。
此時最適合 public、s-maxage + stale-while-revalidate。
更適合在應用/Redis 快取
較為動態,但仍常被重複查詢的資料:
- 重型外部 API 的結果(例如匯率、外部商店的即時價格)。
- 常見的推薦分眾(性別/年齡/場合)。
CDN 並不總是合適,因為資料可能依 token/organization/tenant 而異。在 MCP Gateway 或內部 REST 服務層快取:完全由你掌控,且不會混淆不同使用者的資料。
不可(在共用快取中)快取
與特定使用者綁定的內容:
- 個人訂單與訂單狀態。
- 付款資訊、地址、email。
- 根據私人購買歷史產生的個人化推薦(若屬敏感資訊)。
這些只能在應用層小心快取(且務必避免跨使用者外洩),絕對不能放在 public 的 CDN 快取。
8. Edge 層:CDN 與 edge 函式的差異
別把兩個相似但不同的東西混為一談:
- CDN / edge 快取 — 保存預先計算的回應,幾乎沒有邏輯。
- Edge 函式(Vercel Edge / Cloudflare Workers)— 在 edge 節點上執行的小段程式碼。
經驗顯示:Edge ≠ Serverless。很多開發者把沉重的商業邏輯、LLM 請求、BLOB 處理塞進去,然後被逾時與限制折磨。Edge 函式:
- 啟動極快(幾乎沒有冷啟動)。
- 但受限於 CPU、執行時間與可用 API(通常沒有完整 Node.js、沒有長連線等)。
何時適合用 edge 函式
在 GiftGenius 與 ChatGPT App 的脈絡中,edge 函式適用於:
- 輕量路由:依 locale、x-openai-user-location 或 tenant ID 決定要打哪個區域後端叢集。
- 加入簡單標頭、功能旗標、A/B 路由。
- 快速的唯讀端點,從 edge-KV 或 CDN 快取讀資料,幾乎不做計算。
何時不該用 edge 函式
- 耗時很長的外部 API 請求。
- 呼叫 LLM 模型。
- 複雜的結帳邏輯。
- 具沉重商業邏輯的 MCP 工具。
這些交給一般的 Next.js serverless 函數(例如 runtime = "nodejs"),或乾脆獨立服務/叢集。
Next.js 16 中的 edge 函式範例
做一個小路由 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. Edge 與快取在 GiftGenius 的整體架構
把一切組在一張圖上。
flowchart TD
ChatGPT[(ChatGPT / User)]
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 檢查快取。若有新鮮或「雖舊但仍可用」版本 — 直接回應,甚至不碰 EdgeFn/GW。
- 若沒有快取 — 請求落到 EdgeFn(若啟用)與/或直接進 GW。
- 必要時 GW 使用內部 Redis 快取處理重型操作,或打內部 REST 服務再到資料庫。
- 回應返回後,寫入 CDN/edge 快取,供其他使用者取用。
這樣的設計:
- 降低小工具與 ChatGPT 的延遲。
- 減輕 MCP Gateway 與後端叢集負載。
- 降低 LLM/資料庫的成本(更少重複請求)。
10. GiftGenius 的一些實用片段
分類快取 + Next.js 的 revalidate
前面只談 API 端點。不過 Next.js 對頁面本身也提供類似機制 — 透過 ISR(revalidate)。
以下是 server component,取得分類清單並設定 revalidate = 60:
// 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 打開,而是也作為一般網頁(例如除錯面板或 landing)時很有用。
後端服務中的簡單應用快取
這不是 edge 層,而是應用快取(Redis/記憶體,放在你的 Gift REST API 或其他後端服務中)。 不過順便展示一下最簡單的樣子:
// pseudo-code 於 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,但理念相同:少走一次資料庫/外部 API。
一句話總結:先清楚決定能快取什麼、快取在哪裡(CDN、edge、Redis、資料庫),再去開平台的「魔法」開關。快取不是設定檔的一個勾選,而是架構的一部分:同時影響速度、穩定性與成本。
11. 使用快取與 edge 層的常見錯誤
錯誤 1:「能快取就全快取,只要越快越好」。
經典場景:開發者在所有 JSON 回應上都設 Cache-Control: public, s-maxage=3600。幾小時後發現某使用者看到了別人的訂單,而 ChatGPT 使用了過時的庫存資料。對個人化或敏感資料要麼用 private 快取,要麼乾脆停用 CDN 快取,改在應用層做嚴謹隔離的快取。
錯誤 2:混淆 max-age 與 s-maxage。
有人只設 max-age,以為 CDN 也會照做。實際上 max-age 主要是給瀏覽器,shared 快取要用 s-maxage。結果變成瀏覽器有快取,但 CDN 沒快取,origin 仍被打爆,雖然「我們有設快取」。正確做法是為 CDN 明確指定 s-maxage。
錯誤 3:以為 ETag 能讓一切變快。
ETag 對節省流量很棒,尤其是大型 JSON,但網路往返仍在。對 ChatGPT App 而言,模型仍然要等你的伺服器回應,即使是沒有本體的 304。若你要的是延遲,該用的是 edge 快取 + SWR,ETag 只是輔助。
錯誤 4:把沉重商業邏輯塞進 edge 函式。
「我們直接在 Vercel Edge 呼叫外部 LLM、算複雜精選、再打三個外部 API — 反正很快!」接著就是痛苦:執行時間限制、沒有完整的 Node.js、各種奇怪錯誤。Edge 適合輕量路由與 A/B,重活該放在一般 serverless 函數或獨立後端叢集。
錯誤 5:沒有快取失效策略。
先設了「快取一小時」,一切飛快。然後業務說:「我們改了價格/分類/限制,為什麼 ChatGPT 仍然是舊的?」開發開始手動清快取、重啟服務。對重要資料,必須事先設計如何清快取(由後台 webhook、版本號、key ),而不是指望「它過一小時就會自己更新」。
錯誤 6:忽略快取與成本的關係。
有時開發者只把快取當成速度問題。在 LLM 生態裡這同時是錢的問題:每個多餘的模型與外部 API 呼叫都要錢。沒有快取時,MCP 伺服器可能頻繁敲外部服務/模型,讓月帳單嚇你一跳。正確的快取同時降低延遲與帳單。
錯誤 7:把不同在地/區域資料混在同一個快取。
GiftGenius 在多個國家營運,但快取 key 只用一個 top_gifts。結果:美國使用者看到的是盧布與俄羅斯商店,歐洲使用者看到的是美元與美國商店。做快取時,務必把 locale、currency、tenant 這些維度納入 key 或路徑(例如 /api/{locale}/gifts/top)。
錯誤 8:完全依賴 Next.js/平台的「魔法」。
ISR、revalidate、自動 CDN 都很棒。但若你不了解底層原理,很容易遇到意外:頁面顯示舊內容,API 卻回新資料;ChatGPT 看到的與瀏覽器使用者不同。值得花時間搞懂 Cache-Control、ETag 與 SWR 模式,把 Next.js 當作好用的封裝,而不是黑盒。
錯誤 9:dev/staging/production 沒有在快取策略上區隔。
在開發環境中,快取常妨礙除錯(「我已改資料,為什麼 ChatGPT 還是舊精選?」)。建議有一份設定:dev 幾乎關閉快取(或 TTL 幾秒),production 則開啟積極快取。否則你要嘛在開發時被快取逼瘋,要嘛把 production 不小心用沒快取的設定上線,然後看 MCP Gateway 後面的後端叢集被請求海嘯淹沒。
GO TO FULL VERSION