Caché y revalidación

Configuración de segmentos de ruta, unstable_cache, revalidateTag / revalidatePath y stale-while-revalidate.

Almacena en caché las respuestas de los manejadores de ruta y los datos, y revalídalos periódicamente o bajo demanda. Las entradas obsoletas se sirven de inmediato mientras se ejecuta una actualización en segundo plano.

Manejadores de ruta

Los manejadores de ruta GET son dinámicos por defecto. Activa la caché con la configuración del segmento de ruta:

app/api/posts/route.tsts
export const revalidate = 60;export async function GET() {  const posts = await db.posts.latest();  return Response.json(posts);}
ExportaciónComportamiento
(ninguna)Se ejecuta en cada solicitud
dynamic = "force-static"Se almacena en caché hasta que se revalida
revalidate = NSe almacena en caché; queda obsoleto tras N segundos
revalidate = 0 o dynamic = "force-dynamic"Se ejecuta en cada solicitud

Otros valores de dynamic fallan con TX031. Eso incluye "error" de Next.js, porque Text.js no puede detectar cuándo un manejador lee la solicitud.

Los manejadores con caché tienen una entrada por nombre de ruta, y su request es como sería durante la compilación: sin cadena de consulta, cabeceras, cookies ni cuerpo. Lee esos datos en un manejador dinámico. Solo se almacenan en caché GET y HEAD. Las respuestas con un estado 5xx, una cabecera Set-Cookie o Cache-Control: private / no-store se devuelven, pero nunca se almacenan.

Cabeceras

Las respuestas almacenadas en caché incluyen:

CabeceraValor
x-textjs-cacheMISS, HIT o STALE
Cache-Controls-maxage=N, stale-while-revalidate=<1 year − N>, o s-maxage=31536000 para force-static

Cuando el middleware establece cookies, reescribe la solicitud o modifica sus encabezados, el encabezado generado pasa a ser private, no-cache, no-store, max-age=0, must-revalidate, de modo que las cachés compartidas nunca reutilizan una decisión específica de un visitante. Se conserva cualquier encabezado Cache-Control establecido por el manejador.

En text dev, se aplica la configuración del segmento, pero no se almacena nada, por lo que cada solicitud ejecuta el manejador.

Caché de datos

Almacena en caché cualquier función asíncrona con unstable_cache de @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 },);

La clave se compone de las partes de la clave y los argumentos, codificados por valor (los argumentos undefined y null, Map, Set, Date y URL se mantienen diferenciados). Sin partes de clave, el código fuente de la función y el lugar donde se creó la identifican; pasa partes de clave siempre que la función capture valores de su entorno, por ejemplo, cuando se crea por solicitud o en un bucle. Los resultados se serializan como JSON, por lo que cada llamada devuelve una copia nueva (y los objetos Date se devuelven como cadenas); un resultado que no se pueda serializar lanza una excepción. cache(fn, { key, tags, revalidate }) acepta los mismos ajustes como objeto de opciones.

Dentro de un manejador de ruta con caché, las entradas de datos propagan sus etiquetas y su revalidate más corto a la ruta: revalidar "posts" también actualiza /api/posts. Una llamada de datos con revalidate: 0 hace que la ruta sea dinámica.

Servir datos obsoletos mientras se revalidan

Una vez que la antigüedad de una entrada supera revalidate:

  1. La siguiente solicitud recibe el valor obsoleto de inmediato (STALE).
  2. Se ejecuta una actualización en segundo plano; las solicitudes concurrentes la comparten.
  3. Si la actualización lanza una excepción, se sigue sirviendo el valor obsoleto y se registra el error.

En Vercel y Cloudflare, las actualizaciones en segundo plano se registran con waitUntil(). Las entradas con más de un año de antigüedad siempre se recalculan en primer plano.

Revalidación bajo demanda

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 });}
LlamadaInvalidación
revalidateTag("posts")Todo lo etiquetado con posts; la siguiente solicitud recalcula el valor
revalidateTag("posts", "max")Lo marca como obsoleto; la siguiente solicitud recibe el valor obsoleto mientras se actualiza
revalidatePath("/api/feed")Esa ruta
revalidatePath("/api/posts/[id]")Todas las rutas de una ruta dinámica
revalidatePath("/api", "layout")Todo lo que está bajo /api

Ambas devuelven una promesa. Con el almacén en memoria, la invalidación es inmediata; usa await cuando un almacén personalizado sea asíncrono.

Almacenes de caché personalizados

El almacén predeterminado es un LRU en memoria (1000 entradas, unos 50 MB) por instancia del servidor. Para compartir una caché entre instancias, apunta cacheHandler a un módulo que exporte un CacheStore como exportación predeterminada:

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;

Las entradas son JSON sin más. Como alternativa, setCacheStore(store) instala un almacén en tiempo de ejecución.

Notas sobre plataformas

  • Cada instancia serverless (funciones de Vercel, entornos aislados de Cloudflare) tiene su propia caché en memoria. Cache-Control: s-maxage permite que la CDN de la plataforma también almacene respuestas en caché; la revalidación bajo demanda no purga una CDN. Usa un cacheHandler compartido cuando revalidateTag deba llegar a todas las instancias.
  • Cloudflare requiere la opción nodejs_compat (la caché usa AsyncLocalStorage). Consulta Desplegar.
Editar esta página en GitHub