CodeGym /課程 /ChatGPT Apps /快取與 edge 層:CDN、edge 快取、ETag、SWR、edge 函式(Vercel)及其限制

快取與 edge 層:CDN、edge 快取、ETag、SWR、edge 函式(Vercel)及其限制

ChatGPT Apps
等級 16 , 課堂 4
開放

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

哪裡可以「抄捷徑」?

  1. 在 ChatGPT 與你的周邊之間 — CDN/edge 快取(Vercel CDN/Edge Network),可直接提供不可變資產與可快取的 JSON,而不必打到你的 origin 伺服器。
  2. 在 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-ages-maxage

HTTP 快取主要由 Cache-Control 標頭控制。它決定瀏覽器/ChatGPT 用戶端與/或 CDN 能否快取你的回應,以及可以快取多久。

關鍵重點:

  • max-age — 瀏覽器可快取回應的秒數。
  • s-maxageshared 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 是資源的「指紋」,通常是內容雜湊。運作方式:

  1. 伺服器回應時帶上 ETag: "v1-abc123" 標頭。
  2. 下次請求時,客戶端發送 If-None-Match: "v1-abc123"
  3. 若伺服器判定內容未變,則回應 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-Controlstale-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-Controlmax-age/s-maxage)— 基礎層:允許 CDN 與客戶端快取回應以減輕負載。
  • ETag + If-None-Match — 當需要節省大型 JSON 流量時加入,但要接受一次網路往返。
  • stale-while-revalidate — 當你需要即時回應、即使是略舊資料(目錄、熱門精選)。
  • UI 層 SWRswr/react-query)— 用於平滑小工具重繪、在 ChatGPT 沙盒中維護本地快取。

7. 在 GiftGenius 中該快取什麼、快取多久

試著把 GiftGenius 的資料按「可快取性」分層。

可在 CDN/edge 層快取

這些對所有人(或廣大分眾)相同、且不常變動:

  • 小工具靜態資源:JS/CSS、字型、圖示 — 幾乎「永久」(一年),搭配 immutable
  • 禮物目錄結構:分類、區段、過濾器 — 幾分鐘到幾小時。
  • 通用精選(例如「50 美元以內的同事好禮」)— 幾分鐘到十幾分鐘,旺季尤其適合。

此時最適合 publics-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 函式適用於:

  • 輕量路由:依 localex-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

典型流程:

  1. ChatGPT 小工具請求 /api/gifts/categories
  2. CDN 檢查快取。若有新鮮或「雖舊但仍可用」版本 — 直接回應,甚至不碰 EdgeFn/GW。
  3. 若沒有快取 — 請求落到 EdgeFn(若啟用)與/或直接進 GW。
  4. 必要時 GW 使用內部 Redis 快取處理重型操作,或打內部 REST 服務再到資料庫。
  5. 回應返回後,寫入 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-ages-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。結果:美國使用者看到的是盧布與俄羅斯商店,歐洲使用者看到的是美元與美國商店。做快取時,務必把 localecurrencytenant 這些維度納入 key 或路徑(例如 /api/{locale}/gifts/top)。

錯誤 8:完全依賴 Next.js/平台的「魔法」。
ISR、revalidate、自動 CDN 都很棒。但若你不了解底層原理,很容易遇到意外:頁面顯示舊內容,API 卻回新資料;ChatGPT 看到的與瀏覽器使用者不同。值得花時間搞懂 Cache-ControlETag 與 SWR 模式,把 Next.js 當作好用的封裝,而不是黑盒。

錯誤 9:dev/staging/production 沒有在快取策略上區隔。
在開發環境中,快取常妨礙除錯(「我已改資料,為什麼 ChatGPT 還是舊精選?」)。建議有一份設定:dev 幾乎關閉快取(或 TTL 幾秒),production 則開啟積極快取。否則你要嘛在開發時被快取逼瘋,要嘛把 production 不小心用沒快取的設定上線,然後看 MCP Gateway 後面的後端叢集被請求海嘯淹沒。

1
問卷/小測驗
生產環境與可擴展性,等級 16,課堂 4
未開放
生產環境與可擴展性
生產環境、網路與可擴展性
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION