CodeGym /课程 /ChatGPT Apps /缓存与 edge 层:CDN、edge 缓存、ETag、SWR、edge 函数(Vercel)及其限制

缓存与 edge 层:CDN、edge 缓存、ETag、SWR、edge 函数(Vercel)及其限制

ChatGPT Apps
第 16 级 , 课程 4
可用

1. 为什么在 ChatGPT 应用中要考虑缓存与 edge

在传统 Web 应用中你也会关注速度,但用户至少能看到加载指示器。在 ChatGPT 应用里更有意思:用户与模型对话,模型有时会调用你的应用。小部件需要弹出,并且要尽快展示有用内容。

实践非常明确:latency = 金钱。响应越慢,用户流失概率越高;而额外的 LLM/后端调用会直接增加模型与基础设施成本。缓存能同时降低这两者。

另外,ChatGPT 应用有其特性:

  • 从 ChatGPT 到你的应用的请求会经过网络和多层中间层。每一步的毫秒延迟都会积累。
  • MCP/HTTP 端点有真实的超时(包括 Vercel 无服务器函数与 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 应用 (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,而无需访问源站服务器。
  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-maxage —— 共享缓存(CDN/代理)可缓存的秒数。
  • public —— 允许在共享缓存中缓存。
  • private —— 仅供单个客户端使用;CDN 不缓存。

在 GiftGenius 中,例如:

  • 小部件的 JS/CSS/字体是版本化文件(文件名带哈希),可以放心返回 Cache-Control: max-age=31536000, immutable
  • 礼物分类列表的 JSON —— 对所有用户相同,适合设置 publics-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() {
  // 这里本可以去访问数据库/外部 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 应用场景,它对大型 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 等库,它们在本地维护缓存并后台重新获取数据。

HTTP 响应头中的 SWR

典型响应头:

Cache-Control: public, s-maxage=60, stale-while-revalidate=300

含义:

  • 60 秒,CDN 返回新鲜版本。
  • 61 秒至第 360 秒,CDN 可以立即返回过期响应, 同时在后台向源站拉取新版本。
  • 超过 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 层 —— 以降低对源站的压力。
  • 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/组织/租户。应在 MCP Gateway 或内部 REST 服务层缓存:完全由你控制,且不会混淆不同用户的数据。

不应在公共缓存中缓存

与具体用户绑定的内容:

  • 个人订单及其状态。
  • 支付信息、地址、邮箱。
  • 基于私有订单历史的个性化推荐(如果较为敏感)。

这些只能在应用层以谨慎语义进行缓存(且必须杜绝用户间数据泄漏),绝不能放在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 应用的语境下,edge 函数适合用于:

  • 轻量路由:依据请求头 localex-openai-user-location 或 tenant ID,决定要访问哪个区域后端集群。
  • 添加简单的响应头、功能开关、A/B 路由。
  • 快速的只读端点,从 edge-KV 或 CDN 缓存读取,几乎不做计算。

何时不该用 edge 函数

  • 对外部 API 的长耗时请求。
  • 调用 LLM 模型。
  • 复杂的结账流程逻辑。
  • 带有重型业务逻辑的 MCP 工具。

这些场景请使用常规的 Next.js 无服务器函数(例如 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. GiftGenius 的整体架构中的 Edge 与缓存

把一切放在一张图里。

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 打开,还以普通网页形式使用时(例如调试面板或落地页),这会很有用。

后端服务中的简易应用层缓存

这已经不是 edge 层,而是应用层缓存(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),但思路相同:尽量减少对数据库/外部 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 主要面向浏览器,而共享缓存需要 s-maxage。结果是浏览器缓存了,CDN 却没有,源站仍然压力山大。正确做法是显式为 CDN 指定 s-maxage

错误 3:指望 ETag 让一切都变快。
ETag 很适合为大型 JSON 节省流量,但网络往返仍然存在。在 ChatGPT 应用中,这意味着模型依旧要等待来自你的服务器的响应,即便是没有响应体的 304。如果你追求的是真正的延迟优化,应该使用 edge 缓存 + SWR,ETag 只是辅助手段。

错误 4:把重型业务逻辑塞进 edge 函数。
“我们直接在 Vercel Edge 里调用外部 LLM、做复杂筛选、访问三个外部 API —— 反正 edge 很快!” 然后就是痛苦:执行时间限制、没有完整的 Node.js、各种奇怪错误。Edge 适合轻量路由与 A/B,而重活应该放在常规无服务器函数或独立后端集群中。

错误 5:没有缓存失效策略。
“把缓存设成 1 小时”,一开始一切飞快。过会儿业务说:“我们改了价格/分类/限制,为什么 ChatGPT 里还是旧数据?” 开发者开始手动清缓存、重启服务。对重要数据,应事先设计好如何失效缓存(例如由管理后台的 webhook 触发、按版本、按键清理),而不是指望“再等一小时就好了”。

错误 6:忽视缓存与成本的关系。
有时开发者只把缓存当作性能优化。在 LLM 生态里,这还是成本优化:每一次对模型与外部 API 的多余调用都要花钱。没有缓存,MCP 服务器可能会频繁打外部服务/模型,月底账单会很难看。合理的缓存既降低延迟,也减少账单。

错误 7:把不同语言/区域的数据混在同一个缓存键里。
GiftGenius 在多国运营,但缓存里只用一个键 top_gifts。结果:美国用户看到人民币和中国商店,欧洲用户看到美元和美国商店。缓存时务必在键或路由中加入诸如 localecurrencytenant 等维度(例如 /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 才启用激进缓存。否则要么在开发时被缓存逼疯,要么上线时忘了缓存导致 MCP Gateway 后端集群被请求风暴冲击。

1
调查/小测验
Production 与规模化第 16 级,课程 4
不可用
Production 与规模化
Production、网络与规模化
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION