Next.js
Modelo de caching en Next.js
Mapea las capas de caché de Next.js, sus keys, lifetimes e invalidación: React cache, use cache, output prerenderizado, router, CDN y fuente de datos.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Mapea las capas de caché de Next.js, sus keys, lifetimes e invalidación: React cache, use cache, output prerenderizado, router, CDN y fuente de datos.
Next.js no tiene una sola caché. Una aplicación puede reutilizar funciones dentro de un request, resultados de datos entre requests, output de componentes, rutas en el navegador y respuestas HTTP en CDN. Cada capa posee una key, lifetime e invalidación distintos.
Cuando alguien dice “Next.js está mostrando caché”, primero debes preguntar:
¿qué resultado se reutilizó?
¿en qué proceso o dispositivo?
¿para qué usuarios?
¿durante cuánto tiempo?
¿qué lo invalida?Sin esas respuestas, limpiar la caché o añadir no-store suele atacar la capa equivocada.
1. React request memoization
→ evita trabajo duplicado en el mismo render/request
2. Cache Components / use cache
→ reutiliza funciones, componentes o segmentos
3. Prerendered route output
→ HTML + RSC shell reutilizable
4. Client Router Cache
→ segmentos/RSC payload en memoria del navegador
5. Browser/CDN HTTP cache
→ respuestas gobernadas por headers y plataforma
6. Data source cache
→ Redis, DB buffer, SDK o proveedor externoUna mutación puede requerir actualizar varias capas, pero no siempre todas.
Next.js/React deduplican fetch idénticos dentro del árbol y puedes envolver otras funciones con cache:
import { cache } from "react";
export const getCurrentUser = cache(async () => {
const session = await requireSession();
return userRepository.findById(session.user.id);
});No se invalida manualmente como una caché persistente. Termina con el scope del request.
Creer que React.cache hace que un resultado dure una hora. No define lifetime entre requests.
Con cacheComponents: true, una función, componente o archivo puede declarar reutilización:
export async function getProduct(productId: string) {
"use cache";
cacheLife("hours");
cacheTag(`product:${productId}`);
return productRepository.findById(productId);
}La key incluye:
Esto significa que getProduct("a") y getProduct("b") crean entradas diferentes.
cacheLife({
stale: 300,
revalidate: 3600,
expire: 86400,
});Cuánto tiempo el cliente/router puede considerar el resultado fresco antes de volver al servidor.
A partir de cuándo el servidor puede regenerar el resultado en background.
Límite tras el cual no debe servirse la entrada sin obtener una versión nueva.
Los perfiles seconds, minutes, hours, days, weeks y max expresan políticas comunes.
No elijas un perfil por comodidad. Debe reflejar cuánto contenido obsoleto tolera el negocio.
Entradas con lifetime muy corto pueden excluirse del prerender y convertirse en regiones dinámicas. Una caché de segundos no necesariamente aporta una shell estable.
Si necesitas datos en tiempo real, no fuerces su inclusión en la shell: usa Suspense y request-time.
cacheTag("products");
cacheTag(`product:${productId}`);Una entrada puede tener tags generales y específicas. Esto permite invalidar:
Diseña una convención:
product:{id}
organization:{id}:products
catalog:publicTags no son permisos. No incluyas secretos, y evita cardinalidad ilimitada sin necesidad.
async function ProductCard({ id }: { id: string }) {
"use cache";
const product = await getProduct(id);
return <article>{product.name}</article>;
}Puede reutilizar el resultado React serializado, no solo el dato.
Incluye transformación y markup costoso.
La invalidación queda ligada a la presentación. Cachear funciones de datos suele ofrecer más reutilización entre metadata, pages y handlers.
use cache a nivel de page/layout puede cachear un segmento entero. Es útil cuando toda la entrada comparte lifetime.
Evítalo si dentro existen partes con freshness distinta. La caché cercana al dato permite composición más granular.
children o una Server Action pueden atravesar un componente cacheado si no se inspeccionan:
async function CachedFrame({ children }: { children: React.ReactNode }) {
"use cache";
const navigation = await getNavigation();
return <Shell navigation={navigation}>{children}</Shell>;
}La parte dinámica no forma la key del resultado cacheado como dato inspeccionado; se inserta mediante composición.
cookies(), headers() y searchParams no se leen directamente dentro de use cache normal.
Patrón correcto:
async function PricesForVisitor() {
const cookieStore = await cookies();
const currency = parseCurrency(cookieStore.get("currency")?.value);
return <CachedPrices currency={currency} />;
}La key usa currency, no el objeto cookie completo.
Pasar userId a una caché pública crea una entrada por usuario y puede conservar datos privados. Decide si debe ser request-time, private cache o una store externa segura.
Permite que una plataforma use un handler persistente compartido. Aporta entre réplicas y regiones, pero introduce:
No lo uses para datos baratos o altamente personales sin medir.
Resuelve casos especializados con request data privada. Aumenta cardinalidad y complejidad. La opción predeterminada para datos personales suele ser render dinámico bajo Suspense.
Durante build/regeneración, Next.js puede producir:
Ese output se sirve sin ejecutar todo el árbol en cada request.
Una invalidación de datos etiquetados puede requerir regenerar segmentos que dependían de ellos.
Durante navegación, el navegador guarda segmentos prefetcheados o visitados:
<Link> visible
→ prefetch RSC segment
→ router memory
→ click reutiliza resultadoEsto mejora navegación. No es localStorage ni una caché persistente después de cerrar la pestaña.
Con Cache Components, la información de stale coordina cuánto puede reutilizarse en cliente. Next.js puede preservar rutas recientes mediante React Activity.
router.refresh();Solicita un RSC payload nuevo y combina el árbol.
No garantiza datos frescos si el servidor devuelve la misma entrada cacheada. Tampoco invalida tags.
Úsalo para volver a consultar la route actual cuando la fuente ya cambió o no está cacheada; combina con invalidación cuando exista caché servidor.
Assets con hash suelen usar cache larga. Documentos, Route Handlers e imágenes usan headers específicos.
Cache-Control
ETag
Vary
CDN surrogate keysNo sobrescribas headers administrados por Next.js sin entender sus efectos. Una CDN puede servir una respuesta incluso si la caché interna de datos se invalidó, según configuración de plataforma.
Una key incompleta puede mezclar datos:
getProject(projectId)Si IDs son únicos globalmente y la función devuelve solo datos públicos, puede ser suficiente. En multi-tenant privado:
getProject({ organizationId, projectId, viewerId })Aun así, cachear permisos por viewer puede quedar obsoleto después de revocación.
Estrategias:
Cuando expiran muchas entradas simultáneamente, múltiples requests pueden recalcular:
100 requests
→ same expired key
→ 100 DB queriesLa implementación puede deduplicar fills, pero diseña:
Una caché debería ser optimización, no única copia de datos.
Si Redis o handler remoto falla:
Define la degradación según criticidad.
Buena opción:
Mala opción:
Mide:
Sin métricas, una caché puede ocultar latencia o consumir memoria sin beneficio.
Puedes activar logging de caché compatible:
NEXT_PRIVATE_DEBUG_CACHE=1 pnpm startEn desarrollo, logs de funciones cacheadas pueden aparecer con prefijo. Reproduce también en producción porque el comportamiento de memoria y plataforma difiere.
En Next.js actual, fetch no se cachea por defecto entre requests. Declara política.
Tienen alcance distinto.
Riesgo de fuga.
Pierde rendimiento y genera stampede.
Dificulta freshness diferenciada.
La siguiente petición puede ir a otra instancia.
Solo solicita nueva representación.
export async function getPost(slug: string) {
"use cache";
cacheLife("days");
cacheTag("posts", `post:${slug}`);
return cms.getPost(slug);
}Webhook:
revalidateTag(`post:${slug}`, "max");updateTag desde una Action propia.router.refresh() puede devolver el mismo dato?use cache: remote?projectId en multi-tenant?Revalidation e invalidación explica cómo una mutación o evento cambia el estado de estas entradas sin vaciar todo el sistema.