JavaRush /Курсы /ChatGPT Apps /Кэш и edge-слой: CDN, edge-кэш, ETag, SWR, edge-функции (...

Кэш и edge-слой: CDN, edge-кэш, ETag, SWR, edge-функции (Vercel) и их лимиты

ChatGPT Apps
16 уровень , 4 лекция
Открыта

1. Зачем вообще думать про кэш и edge в ChatGPT App

В классическом веб‑приложении вы тоже волнуетесь о скорости, но пользователь там хотя бы видит спиннер. В 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, чтобы повторные запросы были быстрее и дешевле.
  • Edge‑функции Vercel, чтобы выполнять лёгкую логику максимально близко к ChatGPT и пользователю, но не превращать их в «мини‑бэкенд».

2. Анатомия задержек в GiftGenius и точки кэширования

Сначала полезно честно нарисовать, где вообще рождается latency.

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-клиента (браузер/desktop) Статика виджета, иконки, шрифты (ограниченно контролируем)
CDN / edge-кэш На edge‑узлах Vercel/Cloudflare Статика + общий JSON (категории, конфиги, общие подборки)
Кэш приложений Внутри вашего MCP Gateway или backend‑сервисов (Redis, in‑memory) Результаты тяжёлых запросов к БД/внешним API
БД‑кэш/материализация В самой БД (materialized views и т.п.) Предрассчитанные агрегаты, аналитика

Сейчас сосредоточимся на первых двух: 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() {
  // здесь могли бы ходить в БД/внешнее 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 экономит трафик, но не обязательно уменьшает latency, потому что всё равно нужен round trip до сервера. В контексте ChatGPT Apps это полезно для тяжёлых JSON‑ответов, но ожидать чудесной скорости от одного ETag не стоит — для этого лучше SWR и edge‑кэш.

Пример простого ETag в Next.js‑обработчике (без крипто‑хэшей, чтобы не закапываться):

// 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’и.

SWR в HTTP‑заголовке

Типичный заголовок:

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",
    },
  });
}

SWR в UI‑виджете (React)

Виджет GiftGenius живёт в песочнице ChatGPT и может использовать любой React‑код. Вы уже умеете дёргать свой API через window.fetch. Добавим библиотеку 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, но при этом живём с сетевым round trip.
  • stale-while-revalidate — включаем, когда нам важна мгновенная отдача даже слегка устаревших данных (каталоги, топ‑подборки).
  • SWR в UI (библиотека swr/react-query) — отдельный слой для сглаживания перерисовок виджета и локального кэша в песочнице ChatGPT.

7. Что кэшировать в GiftGenius и как долго

Попробуем разложить данные GiftGenius по «слоям кэшируемости».

Можно кэшировать на уровне CDN/edge

Это всё, что одинаково для всех (или для широких сегментов) и редко меняется:

  • Статика виджета: JS/CSS, шрифты, иконки — условно «навсегда» (год) с immutable.
  • Структура каталогов подарков: категории, разделы, фильтры — минуты/часы.
  • Общие подборки («лучшие идеи для коллег до 50$») — минуты/десятки минут, особенно в пиковые сезоны.

Здесь идеально подходит public, s-maxage + stale-while-revalidate.

Лучше кэшировать в приложении/Redis

Более динамичные, но всё равно повторяющиеся данные:

  • Результаты тяжёлых внешних API (например, вы ходите за курсами валют, за актуальными ценами из внешнего магазина).
  • Часто запрашиваемые сегменты рекомендаций (по полу/возрасту/поводу).

Тут CDN уже не всегда подойдёт, потому что данные могут зависеть от токена/организации/тенанта. Кэшируем на уровне MCP Gateway или внутренних REST‑сервисов: это полностью под вашим контролем и не смешивает данные разных пользователей.

Нельзя кэшировать (в общих кэшах)

То, что привязано к конкретному пользователю:

  • Личные заказы и статусы заказов.
  • Платёжная информация, адреса, email.
  • Конкретные рекомендации на основе приватной истории заказов (если она чувствительна).

Это можно кэшировать только на уровне приложения с аккуратной семантикой (и обязательно без утечки между пользователями), но точно не в public CDN‑кэше.

8. Edge‑слой: CDN против edge‑функций

Важно не путать два похожих, но разных зверя:

  • CDN / edge‑кэш — хранит заранее посчитанные ответы, логики там почти нет.
  • Edge‑функции (Vercel Edge / Cloudflare Workers) — маленькие кусочки кода, которые выполняются на edge‑узлах.

Опыт показывает: Edge ≠ Serverless. Многие разработчики пытаются запихнуть туда тяжёлую бизнес‑логику, запросы к LLM и BLOB‑обработку, а потом удивляются таймаутам и лимитам. Edge‑функции:

  • Стартуют очень быстро (cold start почти нулевой).
  • Но сильно ограничены по CPU, времени выполнения и доступным API (часто без полноценного Node.js, без долгих сокетов и т.д.).

Когда edge‑функция — хорошая идея

В контексте GiftGenius и ChatGPT App edge‑функции полезны для:

  • Лёгкой маршрутизации: по заголовкам locale, x-openai-user-location или tenant ID решить, в какой региональный backend‑кластер отправить запрос.
  • Добавления простых заголовков, фич‑флагов, A/B‑маршрутизации.
  • Быстрых read‑only‑эндпоинтов, которые читают данные из edge‑KV или из CDN‑кэша и практически ничего не считают.

Когда edge‑функция — плохая идея

  • Долгие запросы во внешние API.
  • Вызовы LLM‑моделей.
  • Сложная логика checkout’а.
  • MCP‑инструменты с тяжёлой бизнес‑логикой.

Для всего этого у вас есть обычные serverless‑функции Next.js (например, runtime = "nodejs") или вообще отдельные сервисы/кластера.

Пример edge‑функции в 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. 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 проверяет кэш. Если там свежая или «stale, но ещё годная» версия — сразу отдаёт её, даже не трогая EdgeFn/GW.
  3. Если кэша нет — запрос падает в EdgeFn (если он включён) и/или сразу в GW.
  4. GW при необходимости использует внутренний Redis‑кэш для тяжёлых операций или ходит во внутренние REST‑сервисы и дальше в БД.
  5. Ответ возвращается назад, попадает в CDN/edge‑кэш и раздаётся другим пользователям.

Такое построение:

  • Снижает latency для виджета и ChatGPT.
  • Сокращает нагрузку на MCP Gateway и backend‑кластера.
  • Уменьшает стоимость вызовов 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, но и как обычная веб‑страница (например, debug‑панель или landing).

Простое приложение‑кэш в backend‑сервисе

Это уже не edge‑слой, а кэш приложений (Redis/in‑memory внутри вашего Gift REST API или другого backend‑сервиса). Но к месту показать, как он выглядит в самом простом виде:

// 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: «Кэшируем всё подряд, лишь бы быстрее».
Классика жанра: разработчик ставит Cache-Control: public, s-maxage=3600 вообще на все JSON‑ответы. Через пару часов выясняется, что один пользователь видит заказы другого, а ChatGPT начинает оперировать старыми данными о наличии товара. Для персональных или чувствительных данных нужно либо private‑кэш, либо совсем отключить CDN‑кэш и держать кэш на уровне приложения с аккуратной изоляцией.

Ошибка №2: Путаница между max-age и s-maxage.
Некоторые ставят только max-age и ожидают, что CDN будет кэшировать ровно столько же. На самом деле max-age относится в первую очередь к браузеру, а для shared‑кэша нужен s-maxage. В итоге браузер кэширует, а CDN — нет, и origin продолжает задыхаться под нагрузкой, хотя «кэш же поставили». Правильный путь — явно указывать s-maxage для CDN.

Ошибка №3: Ожидание, что ETag ускорит всё на свете.
ETag отлично экономит трафик, особенно для больших JSON‑файлов, но сетевой round trip всё равно остаётся. В мире ChatGPT App это значит: модель всё равно ждёт ответа от вашего сервера, пусть и 304 без тела. Если вам важна именно задержка, нужен edge‑кэш + SWR, а ETag — это вспомогательный механизм.

Ошибка №4: Попытка засунуть тяжёлую бизнес-логику в edge-функции.
«Давайте будем вызывать внешнюю LLM, считать сложные подборки и ходить в три внешних API прямо из Vercel Edge — там же быстро!» Потом начинается боль: лимиты по времени выполнения, отсутствие нормального Node.js, странные ошибки. Edge хорош для лёгкой маршрутизации и A/B, а вся тяжёлая работа должна идти в обычные serverless‑функции или отдельные backend‑кластера.

Ошибка №5: Отсутствие стратегии инвалидации кэша.
Сделали кэш «на час», всё летает. Потом бизнес говорит: «мы поменяли цены/категории/ограничения, почему в ChatGPT всё по‑старому?» Разработчики начинают дёргать ручки вручную, чистить кэши и перезапускать сервисы. Для важных данных нужно заранее продумать: как вы будете сбрасывать кэш (по webhook’у от админки, по версии, по ключу), а не рассчитывать на «оно само через час обновится».

Ошибка №6: Игнорирование связи кэш ↔ стоимость.
Иногда разработчики думают про кэш только как про скорость. В экосистеме LLM это ещё и про деньги: каждый лишний вызов к модели и к внешнему API стоит денег. Без кэша MCP‑сервер может начать бить внешний сервис/модель так часто, что счёт за месяц неприятно удивит. Правильное кэширование снижает и latency, и bill.

Ошибка №7: Смешивание данных разных локалей/регионов в одном кэше.
GiftGenius работает в нескольких странах, но в кэше используется один ключ top_gifts. Итог: пользователь из США видит рубли и российские магазины, а пользователь из Европы — доллары и магазины из США. При кэшировании всегда учитывайте ключи вроде locale, currency, tenant в имени кэш‑ключа или в маршруте (например, /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 — включает агрессивное кэширование. Иначе вы либо сходите с ума при разработке, либо случайно выкатываете прод без кэша и ловите шторм запросов во внутренние backend‑кластера за MCP Gateway.

1
Задача
ChatGPT Apps, 16 уровень, 4 лекция
Недоступна
Публичный JSON-эндпоинт с Cache-Control для CDN
Публичный JSON-эндпоинт с Cache-Control для CDN
1
Задача
ChatGPT Apps, 16 уровень, 4 лекция
Недоступна
ETag + If-None-Match → 304 Not Modified для конфигурации
ETag + If-None-Match → 304 Not Modified для конфигурации
1
Опрос
Production и масштабирование, 16 уровень, 4 лекция
Недоступен
Production и масштабирование
Production, сеть и масштабирование
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ