Next.js App Router caching: revalidate, dynamic y no-store sin folklore
Qué dice la documentación oficial y qué NO dice
La documentación de caching de Next.js describe cuatro capas de cache que interactúan entre sí:
- Request Memoization - deduplicación de
fetchcon mismo URL dentro de un render. - Data Cache - persistencia de respuestas de
fetchentre requests (configurable conrevalidateono-store). - Full Route Cache - HTML + RSC payload estático generado en build o en runtime.
- Router Cache - cache del lado del cliente para prefetching de segmentos.
Lo que la doc explica bien: cómo configurar cada capa, qué opciones existen, qué semántica tienen.
Lo que la doc no dice: cuál elegir según el tipo de dato que estás sirviendo. Eso es una decisión de producto, no de API. Y ahí es exactamente donde la mayoría se pierde. La doc es honesta sobre los límites del sistema, pero no prescriptiva sobre cuándo cada opción tiene sentido. Esa parte la ponés vos.
Leer los flags como contratos de datos, no como trucos
Antes de escribir una sola línea de configuración, la pregunta correcta no es "¿qué flag uso?". Es: ¿Qué tan viejo puede estar este dato antes de que el usuario note que algo está mal?
Eso es el contrato de datos. Y cada opción de caching en Next.js es una manera de declarar ese contrato explícitamente:
revalidate: N (ISR - revalidación por tiempo)
// Datos que cambian, pero no cada segundo
// Ej: posts de un blog, precios de catálogo con baja rotación
export const revalidate = 3600 // revalida cada hora
Contrato: "Este dato puede tener hasta N segundos de antigüedad. Estoy de acuerdo con eso."
- Cuando tiene sentido: contenido editorial, catálogos de productos que no cambian por hora, páginas de landing con datos semiestructurados.
- Cuando no tiene sentido: stock en tiempo real, datos personalizados por usuario, dashboards operativos.
dynamic = 'force-dynamic'
// Cada request genera un render nuevo en el servidor
// Equivalente a getServerSideProps en Pages Router
export const dynamic = 'force-dynamic'
Contrato: "Este dato no puede tener ninguna antigüedad tolerable. Lo quiero fresco en cada request."
Lo incómodo de este flag: es el más fácil de poner y el más caro de sostener. No hay Full Route Cache, no hay Data Cache para este segmento. Cada usuario que entra paga el costo de un render completo. Eso puede ser exactamente lo correcto - pero tiene que ser una decisión, no el default cuando algo no funciona.
cache: 'no-store' en fetch
// A nivel de fetch individual - más granular que dynamic
const res = await fetch('https://api.ejemplo.com/datos-sensibles', {
cache: 'no-store' // nunca cachear esta respuesta
})
Contrato: "Este dato específico nunca debe cachear, aunque otros datos en el mismo componente sí puedan."
La diferencia con force-dynamic es el alcance. no-store es quirúrgico: afecta un fetch puntual. force-dynamic desactiva el cache del segmento completo. Si necesitás un dato fresco en medio de una page que tiene otros datos cacheables, no-store en el fetch es la herramienta correcta.
Sin configuración (default)
// Sin revalidate, sin dynamic, sin cache: 'no-store'
// Next.js cachea indefinidamente en build time (o hasta invalidación manual)
const res = await fetch('https://api.ejemplo.com/contenido-estatico')
Contrato: "Este dato es estático. Genera una vez, servís siempre, invalidás cuando cambia el código."
Ideal para contenido que no cambia entre deploys: páginas legales, documentación, landings institucionales.
Donde se equivoca la gente (y el costo oculto)
El error más común que veo en proyectos con App Router no es elegir el flag equivocado. Es no elegir conscientemente y dejar que el default decida. Tres patrones problemáticos:
force-dynamiccomo escape hatch universal
Cuando algo "no actualiza bien", el primer instinto es ponerforce-dynamic. Funciona. Pero si el dato podría tolerar 5 minutos de antigüedad, estás pagando el costo de un render por request sin necesidad. No hay número mágico aquí - depende del volumen del sitio y de la infraestructura - pero el costo existe y vale medirlo.Mezclar
revalidateen layout y en page sin entender la herencia
La documentación aclara: el segmento más restrictivo gana. Si el layout tienerevalidate = 0(equivalente aforce-dynamic) y la page tienerevalidate = 3600, el segmento va a renderizar dinámicamente igual. Esto sorprende a mucha gente porque la configuración de la page "parece ignorada".Asumir que
revalidatees exacto
ISR conrevalidate: 60no garantiza que el dato se actualice exactamente a los 60 segundos. La revalidación es stale-while-revalidate: el primer request después de expirar devuelve el dato viejo y dispara la regeneración en background. El siguiente request -que puede ser milisegundos o minutos después, según el tráfico- ya trae el dato nuevo. Si el sistema necesita consistencia estricta por tiempo, ISR no es la herramienta correcta.
Matriz de decisión: qué flag necesita cada dato
Antes de tocar la configuración, respondé estas tres preguntas:
| Pregunta | Respuesta → opción |
|---|---|
| ¿El dato cambia entre deploys? | No → default (estático) |
| ¿Puede tolerar N segundos/minutos de antigüedad? | Sí → revalidate: N |
| ¿Necesita ser fresco en cada request, pero solo este fetch? | Sí → cache: 'no-store' en el fetch |
| ¿Todo el segmento necesita ser fresco en cada request? | Sí → dynamic = 'force-dynamic' |
| ¿El dato depende del usuario autenticado o de headers/cookies? | Sí → force-dynamic o cookies() / headers() (activan dynamic automáticamente) |
Una nota sobre la última fila: en App Router, usar cookies() o headers() dentro de un Server Component activa automáticamente el renderizado dinámico para ese segmento, aunque no declares force-dynamic. Esto es un comportamiento documentado que vale tener claro.
Snippet de referencia: ISR con revalidación por demanda
Este patrón combina revalidate por tiempo con revalidación por demanda vía Route Handler. Es útil cuando el contenido cambia de forma impredecible pero podés disparar una invalidación desde un webhook o un CMS:
// app/blog/[slug]/page.tsx
export const revalidate = 3600 // fallback: revalida cada hora
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const { path } = await request.json()
// Validar que el request viene de una fuente confiable
// (ej: token secreto del CMS)
revalidatePath(path)
return Response.json({ revalidated: true })
}
No es una receta lista para producción - es un punto de partida. La validación de autenticidad del webhook, el manejo de errores y la estrategia de paths son decisiones que dependen de cada sistema.
Límites honestos: qué no podés concluir sin datos reales
Antes de cerrar, quiero ser claro sobre lo que esta guía no prueba:
- No hay benchmarks de rendimiento entre opciones. El impacto depende del volumen de requests, la infraestructura de deploy (Vercel, Railway, self-hosted) y el tiempo de respuesta de los datos externos. Sin medición en contexto real, cualquier número es folklore.
- El comportamiento del Router Cache (cliente) interactúa con el Full Route Cache (servidor) de maneras que pueden sorprender. La documentación oficial describe los casos, pero el comportamiento concreto en una app con navegación compleja conviene validarlo con las DevTools de Next.js o con logs.
- ISR en entornos self-hosted no se comporta exactamente igual que en Vercel. Algunas características de revalidación por demanda asumen infraestructura específica. Revisá la documentación de deploy de Next.js para tu plataforma.
Si estás tomando una decisión de arquitectura de caching en un sistema crítico, estos límites importan. Medí antes de asumir.
FAQ: Next.js App Router caching
¿Cuál es la diferencia entre cache: 'no-store' y dynamic = 'force-dynamic'?
El alcance. cache: 'no-store' afecta un fetch individual: ese dato no cachea, pero el resto del segmento puede seguir cacheando. force-dynamic desactiva el Full Route Cache para el segmento completo: cada request hace un render nuevo en el servidor, sin importar cómo configuren los fetches individuales. Usá no-store cuando necesitás precisión quirúrgica; force-dynamic cuando todo el segmento depende de datos frescos o del contexto del request (usuario, cookies, headers).
¿revalidate: 0 es lo mismo que force-dynamic?
En la práctica, sí. La documentación de Next.js indica que revalidate: 0 equivale a optar por renderizado dinámico. Pero para mayor claridad de intención en el código, force-dynamic es más explícito y menos ambiguo para quien lee el código después.
¿Qué pasa si tengo revalidate diferente en el layout y en la page?
Gana el valor más restrictivo (el más bajo). Si el layout tiene revalidate = 60 y la page tiene revalidate = 3600, el segmento revalida cada 60 segundos. Esto puede hacer que la configuración de la page parezca ignorada, pero es el comportamiento documentado. Revisá siempre la configuración de los layouts que envuelven los pages.
¿Cómo sé si un Server Component está renderizando estático o dinámico?
Con next build podés ver en la salida del terminal qué rutas quedaron estáticas (●), dinámicas (λ) o ISR (◐). También podés usar la barra de herramientas de desarrollo de Next.js en desarrollo local, que indica el modo de renderizado de cada segmento.
¿El Router Cache del cliente interfiere con mis configuraciones de servidor?
Sí, y es una fuente común de confusión. El Router Cache guarda en el cliente los segmentos visitados por un tiempo configurable (distinto al cache del servidor). Podés controlar esto con staleTimes en la configuración de Next.js (disponible desde Next.js 14.2). Si los datos parecen viejos en navegación de vuelta atrás aunque el servidor esté revalidando, el Router Cache es el primer sospechoso.
¿ISR funciona igual en todos los entornos de deploy?
No exactamente. En Vercel, ISR tiene soporte nativo a nivel de CDN. En entornos self-hosted (Railway, VPS, Docker), la revalidación funciona pero depende de la implementación del servidor de Node.js y no necesariamente distribuye el cache entre instancias. La documentación de Next.js tiene una sección específica sobre self-hosting que vale leer antes de asumir paridad de comportamiento.
Cierre: la decisión antes del flag
El caching de Next.js App Router no es complicado cuando lo leés como un sistema de contratos. Cada opción declara algo sobre la frescura que acepta el dato. El trabajo no es memorizar cuál flag hace qué - eso lo resuelve la documentación en dos minutos. El trabajo es tener claro, antes de escribir una línea, cuánta antigüedad puede tolerar cada pieza de información que servís.
Si tenés un dato que cambia cada 5 minutos y lo estás sirviendo con el default estático, estás rompiendo el contrato sin saberlo. Si tenés contenido editorial que podría cachear por horas y usás force-dynamic, estás pagando un costo que nadie aprobó conscientemente.
Mi recomendación práctica: antes de tocar la configuración de cualquier page o layout, escribí en un comentario la respuesta a "¿qué tan viejo puede estar este dato?". Si no podés responder esa pregunta, la conversación técnica no arrancó todavía.
El próximo paso concreto: abrí el output de next build y mirá qué rutas quedaron estáticas, cuáles dinámicas y cuáles ISR. Si hay sorpresas, ahí empieza la investigación real.
Fuentes originales: Next.js caching docs - App Router
Si te interesa el tema de contratos implícitos en infraestructura, tengo posts relacionados sobre Docker healthchecks y qué miden de verdad y firma digital: la diferencia entre formato, certificado y política de validación que siguen la misma lógica.
Este artículo fue publicado originalmente en juanchi.dev
Comments
No comments yet. Start the discussion.