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:
export const revalidate = 60;export async function GET() { const posts = await db.posts.latest(); return Response.json(posts);}| Exportación | Comportamiento |
|---|---|
| (ninguna) | Se ejecuta en cada solicitud |
dynamic = "force-static" | Se almacena en caché hasta que se revalida |
revalidate = N | Se 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:
| Cabecera | Valor |
|---|---|
x-textjs-cache | MISS, HIT o STALE |
Cache-Control | s-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:
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:
- La siguiente solicitud recibe el valor obsoleto de inmediato (
STALE). - Se ejecuta una actualización en segundo plano; las solicitudes concurrentes la comparten.
- 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
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 });}| Llamada | Invalidació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:
export default defineConfig({ cacheHandler: "./cache-handler.ts" });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-maxagepermite que la CDN de la plataforma también almacene respuestas en caché; la revalidación bajo demanda no purga una CDN. Usa uncacheHandlercompartido cuandorevalidateTagdeba llegar a todas las instancias. - Cloudflare requiere la opción
nodejs_compat(la caché usaAsyncLocalStorage). Consulta Desplegar.