Кеширование и ревалидация

Конфигурация сегментов маршрута, unstable_cache, revalidateTag / revalidatePath и stale-while-revalidate.

Кешируйте ответы обработчиков маршрутов и данные, затем ревалидируйте их по таймеру или по требованию. Устаревшие записи возвращаются сразу, пока в фоне выполняется одно обновление.

Обработчики маршрутов

Обработчики маршрутов GET по умолчанию динамические. Включите кеширование в конфигурации сегмента маршрута:

app/api/posts/route.tsts
export const revalidate = 60;export async function GET() {  const posts = await db.posts.latest();  return Response.json(posts);}
ЭкспортПоведение
(нет)Выполняется при каждом запросе
dynamic = "force-static"Кешируется до ревалидации
revalidate = NКешируется; устаревает через N секунд
revalidate = 0 или dynamic = "force-dynamic"Выполняется при каждом запросе

Другие значения dynamic вызывают ошибку TX031. В их числе "error" из Next.js, поскольку Text.js не может определить, когда обработчик читает запрос.

Для кешируемых обработчиков создаётся одна запись на каждый путь, а их request выглядит так же, как во время сборки: без строки запроса, заголовков, cookie и тела. Читайте их в динамическом обработчике. Кешируются только GET и HEAD. Ответы со статусом 5xx, заголовком Set-Cookie или Cache-Control: private / no-store возвращаются, но никогда не сохраняются.

Заголовки

Кешированные ответы содержат:

ЗаголовокЗначение
x-textjs-cacheMISS, HIT или STALE
Cache-Controls-maxage=N, stale-while-revalidate=<1 year − N> или s-maxage=31536000 для force-static

Когда middleware устанавливает cookie, переписывает путь или переопределяет заголовки запроса, сгенерированный заголовок становится private, no-cache, no-store, max-age=0, must-revalidate, поэтому общие кеши никогда не воспроизводят решение, принятое для отдельного посетителя. Заголовок Cache-Control, заданный обработчиком, сохраняется.

В text dev конфигурация сегмента применяется, но ничего не сохраняется, поэтому каждый запрос запускает обработчик.

Кеш данных

Кешируйте любую асинхронную функцию с помощью unstable_cache из @textjs/app/cache:

lib/posts.tsts
import { unstable_cache } from "@textjs/app/cache";export const getPosts = unstable_cache(  async (author: string) => db.posts.byAuthor(author),  ["posts"],  { tags: ["posts"], revalidate: 300 },);

Ключ состоит из частей ключа и аргументов, закодированных по значению (аргументы undefined и null, Map, Set, Date и URL остаются различимыми). Если части ключа не заданы, функцию идентифицируют её исходный код и место создания; передавайте части ключа, когда функция захватывает значения в замыкании, например при создании для каждого запроса или в цикле. Результаты сериализуются в JSON, поэтому каждый вызов возвращает новую копию (а объекты Date возвращаются в виде строк); если результат нельзя сериализовать, выбрасывается исключение. cache(fn, { key, tags, revalidate }) принимает те же настройки в виде объекта параметров.

Внутри кешируемого обработчика маршрута записи данных передают маршруту свои теги и наименьшее значение revalidate: ревалидация "posts" обновляет и /api/posts. Вызов данных с revalidate: 0 делает маршрут динамическим.

Выдача устаревших данных при ревалидации

Когда возраст записи превышает revalidate:

  1. Следующий запрос немедленно получает устаревшее значение (STALE).
  2. В фоне выполняется одно обновление; параллельные запросы используют его совместно.
  3. Если обновление выбрасывает исключение, устаревшее значение продолжает возвращаться, а ошибка записывается в журнал.

На Vercel и Cloudflare фоновые обновления регистрируются через waitUntil(). Записи старше года всегда пересчитываются в основном потоке.

Ревалидация по требованию

app/api/revalidate/route.tsts
import { revalidatePath, revalidateTag } from "@textjs/app/cache";export async function POST(request: Request) {  // Anyone can POST here: require a secret shared with your CMS or webhook.  if (request.headers.get("authorization") !== `Bearer ${process.env.REVALIDATE_SECRET}`) {    return new Response("Unauthorized", { status: 401 });  }  await revalidateTag("posts");  await revalidatePath("/api/feed");  return Response.json({ revalidated: true });}
ВызовЧто инвалидируется
revalidateTag("posts")Всё с тегом posts; следующий запрос запускает пересчёт
revalidateTag("posts", "max")Помечает данные как устаревшие; следующий запрос получает устаревшие данные, пока выполняется обновление
revalidatePath("/api/feed")Указанный путь
revalidatePath("/api/posts/[id]")Каждый путь динамического маршрута
revalidatePath("/api", "layout")Всё внутри /api

Обе функции возвращают промис. Для хранилища в памяти инвалидация происходит немедленно; используйте await, если пользовательское хранилище асинхронное.

Пользовательские хранилища кеша

Хранилище по умолчанию — LRU в памяти (1000 записей, около 50 МБ) для каждого экземпляра сервера. Чтобы использовать общий кеш для нескольких экземпляров, укажите в cacheHandler модуль, который экспортирует CacheStore по умолчанию:

text.config.tsts
export default defineConfig({ cacheHandler: "./cache-handler.ts" });
cache-handler.tsts
import type { CacheStore } from "@textjs/app/cache";const store: CacheStore = {  async get(key) { return redis.get(key).then((v) => (v ? JSON.parse(v) : undefined)); },  async set(key, entry) { await redis.set(key, JSON.stringify(entry)); },  async delete(key) { await redis.del(key); },  async markTags(tags, mode, at) { for (const t of tags) await redis.hset(`tag:${t}`, mode, at); },  async getTagState(tags) { /* max expiredAt / staleAt across tags */ return { expiredAt: 0, staleAt: 0 }; },};export default store;

Записи хранятся в обычном JSON. Вместо этого setCacheStore(store) устанавливает хранилище во время выполнения.

Примечания по платформам

  • У каждого serverless-экземпляра (функции Vercel, изолята Cloudflare) собственный кеш в памяти. Cache-Control: s-maxage позволяет CDN платформы тоже кешировать ответы; ревалидация по требованию не очищает CDN. Используйте общий cacheHandler, если revalidateTag должен затрагивать каждый экземпляр.
  • Cloudflare требует флаг nodejs_compat (кеш использует AsyncLocalStorage). См. Развёртывание.
Изменить эту страницу на GitHub