Escritos

use cache en Next.js 16: caché del lado del servidor con Cache Components

use cache ya no es canary: llega estable con Next.js 16 como parte de Cache Components. Cómo activarlo, cómo se genera la clave de caché, por qué cookies() no funciona dentro y en qué casos sigues necesitando unstable_cache.

Furkan Özay15 de junio de 2025 · 11 min de lectura
czCZAY.DEV // EDITORIALES
MODERN WEB & REACT

#React · 11 MIN DE LECTURA

FURKAN ÖZAYVOL. 2025 // #1707

Importante: Este artículo se ha actualizado

Cuando lo escribí por primera vez, use cache estaba en la versión canary de Next.js 15 y decía "no lo uses en producción". Ya no es así: con Next.js 16.0 pasó a la versión estable como parte de la función Cache Components. He reescrito el artículo desde cero según la documentación de 16.x: aquí tienes tanto el uso correcto como los puntos donde más gente se atasca.

Durante mucho tiempo, el caché del lado del servidor en Next.js fue un tema disperso: un comportamiento propio para las llamadas a fetch, una API aparte para unstable_cache y React.cache para las repeticiones dentro de una misma solicitud. use cache sustituye a los tres con una única directiva: cacheas la salida de una función, de un componente o de un archivo entero, y Next.js genera la clave por sí mismo.

Primero: sin activarlo, no funciona

Este es el paso que más se pasa por alto. Escribir use cache no basta; tienes que activar la función Cache Components:

Ejemplo: next.config.ts

TypeScript
import type { NextConfig } from "next";
 
const nextConfig: NextConfig = {
	cacheComponents: true,
};
 
export default nextConfig;

Si escribes la directiva sin activarlo, no se cachea nada y todo sigue funcionando en silencio, sin que veas ningún error. Es lo que más tiempo te hace perder cuando buscas el motivo de la lentitud.

¿Dónde se escribe?

Hay tres lugares y en los tres la función tiene que ser async:

Ejemplo: Nivel de función, de componente y de archivo

TSX
// 1. Fonksiyon seviyesi
export async function getData() {
	"use cache";
	const res = await fetch("https://api.example.com/data");
	return res.json();
}
 
// 2. Bileşen seviyesi — prop'lar önbellek anahtarına girer
export async function Bookings({ type = "haircut" }: { type: string }) {
	"use cache";
	const res = await fetch(`https://api.example.com/bookings?type=${type}`);
	return <List items={await res.json()} />;
}
TSX
// 3. Dosya seviyesi — dosyadaki her export önbelleğe alınır
"use cache";
 
export async function getMonthlyTotals(accountId: string) {
	return db.orders.aggregate({ where: { accountId }, _sum: { amount: true } });
}
 
export async function getTopProducts() {
	return db.products.findMany({ orderBy: { sales: "desc" }, take: 10 });
}

Si lo usas a nivel de archivo, todos los exports quedan incluidos, también generateMetadata y generateStaticParams; todos tienen que ser async. Si quieres generar por adelantado un segmento de ruta por completo, debes poner la directiva al principio de cada archivo que forme esa ruta (page, layout, slots de rutas paralelas), uno por uno: cada segmento es una entrada de caché independiente.

¿Cómo se genera la clave de caché?

La clave se compone de cuatro partes:

  1. Build ID: cambia en cada compilación, es decir, cada despliegue invalida toda la caché
  2. Identidad de la función: la ubicación de la función en el código y un resumen seguro de su firma
  3. Argumentos serializables: las props en un componente, los parámetros en una función
  4. Resumen de HMR: solo en desarrollo, descarta la caché con el hot reload

El matiz importante no está en el cuarto punto, sino en los argumentos: las variables que usas del ámbito exterior también se cuentan automáticamente como argumentos y entran en la clave.

Importante: El cierre (closure) forma parte de la clave

TSX
async function Component({ userId }: { userId: string }) {
	const getData = async (filter: string) => {
		"use cache";
		// Anahtar hem userId'yi (kapanıştan) hem filter'ı (argümandan) içerir
		const res = await fetch(
			`https://api.example.com/users/${userId}/data?filter=${filter}`
		);
		return res.json();
	};
 
	return getData("active");
}

La buena noticia: no vas a mostrar por error los datos de un usuario a otro. La mala: sin darte cuenta puedes diversificar la clave y reducir la tasa de aciertos de la caché. Recuerda que cada variable externa que uses dentro divide la clave.

Cuánto tiempo se mantendrá fresco: cacheLife

Si no llamas a cacheLife, se aplica el perfil default:

  • stale: 5 minutos (lado del cliente)
  • revalidate: 15 minutos (lado del servidor)
  • expire: nunca expira con el tiempo

Consejo: Escribe la duración de forma explícita

TSX
import { cacheLife } from "next/cache";
 
export async function getOrderSummary(accountId: string) {
	"use cache";
	cacheLife("hours");
 
	const orders = await getOrders(accountId);
	return { orders };
}

La recomendación de la documentación es clara: escribe cacheLife de forma explícita en cada ámbito de use cache. Si no, el comportamiento no se ve en el punto de la llamada y depende del valor por defecto o de la caché que lo envuelve.

Una advertencia: si colocas otro use cache de corta duración dentro de un use cache que no tiene cacheLife, la compilación da error. En cachés anidadas tienes que elegir las duraciones de forma deliberada.

Etiqueta y limpia a mano: cacheTag

La actualización basada en el tiempo no siempre es la respuesta correcta. Si quieres limpiar la caché al instante cuando cambia el contenido, usa etiquetas:

Ejemplo: cacheTag + updateTag

TSX
// lib/products.ts
import { cacheTag } from "next/cache";
 
export async function getProducts() {
	"use cache";
	cacheTag("products");
	const res = await fetch("https://api.example.com/products");
	return res.json();
}
TSX
// app/actions.ts
"use server";
 
import { updateTag } from "next/cache";
 
export async function updateProduct() {
	await db.products.update(/* ... */);
	updateTag("products"); // "products" etiketli tüm girdiler düşer
}

En la práctica se usan juntos: un artículo de blog que cambia rara vez recibe un cacheLife largo más cacheTag y se limpia solo cuando se guarda; una lista que fluye a lo largo del día se refresca sola con un perfil corto.

Donde más gente se atasca: cookies() no funciona dentro

Dentro de un ámbito cacheado no se pueden usar cookies(), headers() ni searchParams. Además, la restricción sigue la pila de llamadas: si una función auxiliar que llamas desde dentro lee alguno de ellos, también falla (next-request-in-use-cache).

Precaución: La parte traicionera

En una ruta que se renderiza de forma dinámica, este error aparece en tiempo de ejecución, no en la compilación. Es decir, next build puede pasar sin problemas y fallar con next start. Verde en CI, rojo en producción.

El patrón correcto: lee los datos de la solicitud fuera del ámbito de la caché y pásalos dentro como argumento.

Explicación: Incorrecto / correcto

TSX
// YANLIŞ — çalışma anında patlar
async function getCart() {
	"use cache";
	const store = await cookies();
	return db.cart.find({ id: store.get("cart")?.value });
}
 
// DOĞRU — okuma dışarıda, argüman içeride
export default async function Page() {
	const store = await cookies();
	const cartId = store.get("cart")?.value;
	return <Cart cartId={cartId} />;
}
 
async function Cart({ cartId }: { cartId?: string }) {
	"use cache";
	return <CartView data={await db.cart.find({ id: cartId })} />;
}

Para los casos en que realmente es inevitable existe use cache: private, pero la documentación lo marca como "muy raramente": para requisitos de cumplimiento normativo o situaciones en las que no puedes refactorizar el código.

Límites de serialización

Los argumentos y los valores devueltos tienen que ser serializables.

Válidos: tipos primitivos, objetos planos, arrays, Date, Map, Set, TypedArray, ArrayBuffer. El valor devuelto también puede ser JSX.

No válidos: instancias de clases, funciones, Symbol, WeakMap/WeakSet y, algo que se olvida fácilmente, instancias de URL.

El detalle salvador aquí: puedes pasar un valor y usarlo sin mirar dentro de él. Así funcionan children y las Server Actions.

Consejo: El patrón de paso (pass-through)

TSX
async function CachedWrapper({ children }: { children: ReactNode }) {
	"use cache";
	// children okunmuyor, sadece geçiriliyor — anahtarı etkilemiyor
	return (
		<div className="wrapper">
			<header>Önbelleğe alınmış başlık</header>
			{children}
		</div>
	);
}
 
export default function Page() {
	return (
		<CachedWrapper>
			<DynamicComponent /> {/* önbelleğe alınmıyor */}
		</CachedWrapper>
	);
}

Así puedes cachear el shell estático de la página e insertar dentro la parte dinámica. Poner use cache en un layout no cachea los children que hay debajo.

YouTube

También hay un vídeo explicativo de este tema.

Ir al canal →

¿Dónde se guarda? La forma de alojamiento cambia el resultado

El manejador por defecto es una LRU en memoria. Esto se comporta de forma muy distinta según dónde publiques:

EntornoComportamiento en tiempo de ejecución
ServerlessLas entradas normalmente no persisten entre solicitudes; cada solicitud puede caer en una instancia distinta. El cacheo en tiempo de compilación funciona con normalidad.
Tu propio servidorLas entradas persisten entre solicitudes. El tamaño se ajusta con cacheMaxMemorySize.

Advertencia: Una deducción errónea frecuente

La mayoría de las quejas del tipo "puse use cache pero la carga de la base de datos no bajó" vienen de esta tabla. En serverless la caché en memoria no es persistente. Si de verdad quieres una caché compartida, tienes que conectar un manejador aparte (Redis, KV) con use cache: remote, y eso también tiene latencia de red y coste de plataforma.

Además, ninguna directiva de caché se traslada a un nuevo despliegue, porque la clave incluye el build ID. Para los datos que deben persistir entre despliegues sigues necesitando unstable_cache o la caché de fetch. use cache no lo sustituye todo.

Si la compilación se atasca a los 50 segundos

Si ves este error, la causa casi siempre es la misma:

Filling a cache during prerender timed out, likely because request-specific arguments such as params, searchParams, cookies() or uncached data were used inside "use cache".

El ámbito cacheado está esperando una Promise que no se puede resolver durante la compilación. Las tres vías típicas: pasar una Promise así como prop, acceder a ella desde un closure o obtenerla a través de un Map compartido. La solución es la misma: haz el await del valor fuera y pasa datos planos al interior.

En cambio, llamar a cookies() directamente dentro no da un timeout sino un error inmediato: no los confundas.

Otros dos comportamientos que debes conocer

Con el modo borrador (Draft Mode) activado, todas las funciones cacheadas se vuelven a ejecutar en cada solicitud y el resultado no se guarda. No hay riesgo de ver contenido obsoleto en la vista previa; no necesitas escribir código adicional para ello.

React.cache está aislado. Un valor que guardes con React.cache fuera del límite de use cache no es visible desde dentro. La única forma de meter datos es mediante un argumento.

¿Cuándo no usarlo?

  • No es compatible con la exportación estática (output: "export"). Se admiten el servidor Node.js y Docker.
  • En datos específicos del usuario que cambian en cada solicitud: tendrías que sacarlos del ámbito y pasarlos como argumento, lo cual la mayoría de las veces vuelve inútil la caché.
  • En cálculos costosos que deben sobrevivir entre despliegues: la clave con el build ID los reinicia.
  • Si la frescura de los datos se mide en segundos: el enrutador del cliente impone un tiempo de obsolescencia de al menos 30 segundos, independientemente de la configuración.

Depuración

Consejo: Registro detallado

Bash
NEXT_PRIVATE_DEBUG_CACHE=1 npm run dev
# ya da üretim için
NEXT_PRIVATE_DEBUG_CACHE=1 npm run start

En desarrollo, las salidas de console.log de las funciones cacheadas aparecen con el prefijo Cache: la forma más rápida de saber qué llamada se devolvió desde la caché.

Resumen

Resumen: En pocas palabras

  • use cache es estable en Next.js 16.0; se activa con cacheComponents: true
  • Funciona a nivel de función, componente, archivo y segmento de ruta; todos deben ser async
  • La clave: build ID + identidad de la función + argumentos + variables capturadas del closure
  • La duración con cacheLife, la limpieza manual con cacheTag + updateTag
  • cookies(), headers(), searchParams están prohibidos dentro: léelos fuera y pásalos como argumento
  • El almacenamiento por defecto es LRU en memoria; en serverless no persiste entre solicitudes
  • Si necesitas persistencia entre despliegues, sigue siendo unstable_cache o la caché de fetch

La primera versión de este artículo tenía una sección de "prueba de rendimiento" con cifras como una mejora del 78 %. La he quitado porque esas cifras no provenían de una medición real, sino de un montaje inventado con setTimeout, y no te servían para estimar la ganancia en tu proyecto. La ganancia depende por completo de lo que cachees: cachear una consulta a la base de datos que tarda 200 ms no es lo mismo que cachear un cálculo que tarda 2 ms. Haz tu propia medición con NEXT_PRIVATE_DEBUG_CACHE=1.

Nota: Fuente

Este artículo se basa en la documentación oficial de Next.js 16.x (directiva use cache). Historial de versiones: v15.0.0 la introdujo de forma experimental, v16.0.0 la hizo estable con Cache Components.

Hoja de ruta y herramienta interactivas

Sigue leyendo