El caching de Next.js tiene mala fama, y no es injusta. Pero la causa rara vez se nombra bien: el problema no es que sea difícil, es que cambió de filosofía tres veces en tres años. Lo que aprendiste en 2023 estaba medio obsoleto en 2024 y se reorganizó entero en la v16. Cada release movió los defaults, y cuando los defaults se mueven, el conocimiento se pudre.
Así que en vez de darte una receta que caduca en el próximo major, vamos a recorrer la evolución. Entender de dónde viene Cache Components —el modelo de Next.js 16— es la única forma de que deje de parecer magia y empiece a parecer una decisión.
el problema de fondo
Todo caching es el mismo trato: cambias frescura por velocidad. Guardas un resultado para no volver a calcularlo, y a cambio aceptas que pueda quedar viejo. Esa tensión no la inventó Next.js y no la va a resolver nadie; lo único que cambia entre frameworks es quién decide y qué tan visible es la decisión.
La apuesta original del App Router fue ambiciosa: automatizar ese trato. Que el framework dedujera qué cachear y por cuánto tiempo, para que tú no tuvieras que pensarlo. La intención era buena. El resultado fue que mucha gente tenía datos cacheados sin saber por qué, y datos frescos sin saber por qué. La queja más repetida de toda la era App Router —“¿por qué mi página no se actualiza?”— nace justo aquí.
Para entender el porqué, hay que abrir la caja. No hay una caché en Next.js: hay cuatro.
los cuatro cachés (el modelo clásico, v13–v14)
Cuando salió el App Router, Next.js apilaba cuatro cachés distintos, cada uno en una capa diferente del recorrido de un request. La confusión venía de que casi nadie sabía que eran cuatro: los vivías como un solo comportamiento errático.
| Caché | Dónde vive | Qué guarda | Duración |
|---|---|---|---|
| Request Memoization | Servidor, en memoria | Resultado de un fetch repetido | Un solo render |
| Data Cache | Servidor, persistente | Resultado de fetch entre requests | Hasta revalidar |
| Full Route Cache | Servidor, build | HTML + RSC payload de rutas estáticas | Hasta redeploy/revalidar |
| Router Cache | Cliente, en memoria | Segmentos de ruta ya visitados | Sesión / staleTime |
1. Request Memoization
Es el más inofensivo y el único que no es realmente de Next.js, sino de React. Si llamas al mismo fetch (misma URL, mismas opciones) varias veces dentro de un mismo render —por ejemplo, el layout y la página piden el mismo usuario—, React lo ejecuta una sola vez y reparte el resultado.
Su alcance es un único pase de renderizado: se crea cuando empieza el render y se tira a la basura cuando termina. No persiste entre requests. Es deduplicación, no caché en el sentido de “datos viejos”, y por eso casi nunca te muerde.
2. Data Cache
Aquí empieza lo serio. El Data Cache persiste de verdad: guarda el resultado de tus fetch en el servidor, entre requests distintos e incluso entre deployments. Es la pieza sobre la que se construía el ISR (Incremental Static Regeneration).
Y aquí estaba el default que causó la mitad del dolor: en la v13 y v14, fetch se cacheaba por defecto (cache: 'force-cache'). Pedías datos a tu API, Next los guardaba indefinidamente, y tu página mostraba lo de hace una hora sin que tú hubieras pedido nada. Controlarlo se hacía con opciones del propio fetch:
// v13/v14: cacheado por defecto, lo controlas con opciones
await fetch(url) // cacheado para siempre
await fetch(url, { cache: 'no-store' }) // nunca cacheado
await fetch(url, { next: { revalidate: 3600 } }) // ISR: revalida cada hora
await fetch(url, { next: { tags: ['products'] } })// invalidable por tag
El problema no era técnico, era de ergonomía: el comportamiento por defecto era invisible y pegajoso. Y solo aplicaba a fetch; si pedías datos con el cliente de Prisma o un SDK, no entrabas en el Data Cache y tenías que cachear a mano con unstable_cache.
3. Full Route Cache
Un nivel más arriba. Cuando una ruta es estática, Next la prerenderiza en build y guarda el resultado completo: el HTML y el RSC payload. En producción esa ruta se sirve desde la caché sin volver a ejecutar el render. Es lo que hace que una landing vuele.
El Full Route Cache está atado al Data Cache: una ruta estática se considera fresca mientras los datos de los que depende lo estén. Se invalida al redeployar o cuando revalidas los datos subyacentes. La consecuencia que confundía a todos: una página podía quedarse “congelada” en build porque, sin tú saberlo, todos sus datos eran cacheables y por tanto la ruta entera era estática.
4. Router Cache
El único que vive en el navegador. Cuando navegas entre páginas (soft navigation), Next guarda en memoria el RSC payload de los segmentos que ya visitaste. Si vuelves atrás, la vista se restaura al instante sin tocar el servidor.
Suena bien, y lo es, hasta que muerde: en la v14, este caché podía servirte una versión vieja de una página dinámica durante segundos después de que los datos cambiaran, porque su frescura no se controlaba con los datos sino con un tiempo de staleness del cliente. “Hice la mutación, navegué, y sigo viendo lo viejo” casi siempre era esto.
por qué dolía
Junta las cuatro capas y tienes el problema cultural del App Router: demasiada magia implícita. El caching era el comportamiento por defecto en tres de las cuatro capas, las decisiones eran invisibles, y depurar “por qué esto está viejo” te obligaba a tener un modelo mental de las cuatro a la vez. Encima, dos de ellas (fetch cacheado, route handlers cacheados) cacheaban cosas que la mayoría de la gente esperaba que fueran frescas.
El propio equipo de Next lo reconoció. La dirección quedó clara: menos magia, defaults más predecibles, y volver el caching explícito.
v15: el giro hacia lo explícito
Next.js 15 (octubre de 2024) fue, sobre todo, una corrección de defaults. No reescribió las cuatro capas; les quitó la pegajosidad invisible:
fetchya no se cachea por defecto. Pasa ano-store. Si quieres cachear, lo pides explícitamente. Esto solo invierte el default que más confundía.- Los GET Route Handlers ya no se cachean por defecto. Antes un
route.tscon GET era estático sin avisar; ahora es dinámico salvo que optes porforce-static. - El Router Cache deja de cachear páginas dinámicas (staleTime de
0por defecto para segmentos de página). Adiós al “navegué y veo lo viejo”. - Las APIs de request se vuelven asíncronas:
cookies(),headers(),paramsysearchParamsahora sonawait. Es la pista que el framework necesita para saber, sin ambigüedad, que estás tocando algo dinámico.
// v15+: cookies/headers/params/searchParams son async
import { cookies } from 'next/headers'
export default async function Page() {
const session = (await cookies()).get('session')
// ...
}
La filosofía cambió de “cacheamos por ti y tú desactivas” a “tú decides cuándo cachear”. Pero seguían siendo cuatro capas con cuatro mecanismos de control distintos. El siguiente paso fue unificarlos.
v16: Cache Components
Next.js 16 (21 de octubre de 2025) es donde el modelo se reordena de verdad. La pieza central se llama Cache Components y se activa con un flag:
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
cacheComponents: true,
}
export default nextConfig
El cambio mental es total. En el modelo clásico, cachear era el default y tú desactivabas. En Cache Components es al revés: todo es dinámico por defecto y cachear es opt-in. No cacheas una ruta entera ni configuras capas: marcas explícitamente qué pedazo de la UI es cacheable, con una directiva.
Y activarlo convierte a PPR en el comportamiento por defecto, retirando los antiguos flags experimentales experimental.ppr y experimental.dynamicIO (Cache Components era, de hecho, el nombre nuevo de dynamicIO). PPR son las siglas de Partial Prerendering: prerenderizado parcial, es decir, prearmar solo una parte de la página en vez de todo o nada.
el nuevo default: una parte fija y otra que llega después
Piensa en una página de producto. El encabezado, la descripción y las fotos son iguales para todos y no cambian a cada rato. Pero el bloque de “recomendado para ti” depende de quién eres. Antes tenías que elegir entre dos extremos: o toda la página era estática (rápida, pero ese bloque personal quedaba viejo) o toda era dinámica (siempre fresca, pero lenta para todos).
El prerenderizado parcial rompe ese “todo o nada”. Sirve al instante la parte que es igual para todos —ya viene prearmada— y deja unos huecos para las piezas personalizadas, que se rellenan un segundo después sin obligar al usuario a mirar una página en blanco. Una sola respuesta: lo fijo primero, lo personal en cuanto está listo.
¿Y cómo sabe Next qué va en cada parte? Con <Suspense>. Todo lo que envuelvas en <Suspense> es un hueco que se rellena después; el resto es la parte fija. Por eso, con cacheComponents activo, cualquier código que lea datos del usuario (cookies(), headers(), un query sin cachear) tiene que ir dentro de un <Suspense> — si no, Next te detiene el build y te avisa. Ese <Suspense> es, literalmente, la línea que separa lo fijo de lo que llega después.
import { Suspense } from 'react'
export default function ProductPage() {
return (
<>
<ProductDetails /> {/* estático, va en el shell */}
<Suspense fallback={<Skeleton />}>
<Recommendations /> {/* dinámico, llega por streaming */}
</Suspense>
</>
)
}
cachear de verdad: use cache
Para datos compartidos que toleran estar “un poco viejos” —el reemplazo directo del ISR— marcas la función o el componente con 'use cache', y controlas la frescura con cacheLife y la invalidación con cacheTag:
import { cacheLife, cacheTag } from 'next/cache'
async function getProducts() {
'use cache'
cacheLife('hours') // perfil de frescura
cacheTag('products') // etiqueta para invalidar on-demand
return db.query('SELECT * FROM products')
}
Fíjate en lo que cambió respecto al modelo viejo: ya no cacheas a nivel de fetch ni de ruta, y ya no importa si los datos vienen de fetch, de Prisma o de donde sea. Cacheas el valor de retorno de una función. Es el mismo mecanismo para todo.
cacheLife maneja tres tiempos —stale (cuánto puede el cliente usar lo cacheado sin preguntar), revalidate (tras esto, el siguiente request dispara un refresh en background) y expire (tras esto sin requests, el siguiente espera contenido fresco)— y acepta perfiles con nombre ('hours', 'days', 'max') o uno a medida:
cacheLife({ stale: 30, revalidate: 120, expire: 600 }) // segundos
invalidar: revalidateTag y updateTag
Tras una mutación, invalidas por tag. Y aquí hay dos cambios que te van a morder si vienes de v15:
'use server'
import { revalidateTag, updateTag } from 'next/cache'
export async function updateProduct(id: string, data: ProductData) {
await db.products.update({ where: { id }, data })
revalidateTag('products', 'max') // ⚠️ ahora pide un 2º argumento
}
revalidateTagahora requiere un segundo argumento: un perfil decacheLifeque habilita el comportamiento stale-while-revalidate. La firma de un solo argumento quedó deprecada; el valor recomendado es'max'.updateTages nuevo y es solo para Server Actions. Da semántica read-your-writes: invalida de forma síncrona en el mismo request, para que el usuario que acaba de hacer un cambio lo vea de inmediato en vez de la versión vieja.revalidateTages eventual;updateTages ahora mismo.
cómo mapean los viejos cachés al nuevo modelo
Si ya tenías el modelo de cuatro capas en la cabeza, esta tabla es el puente. Las capas no desaparecieron; se volvieron explícitas o se escondieron bien:
| Modelo clásico | En Cache Components (v16) |
|---|---|
| Request Memoization | Sigue igual (es de React); no lo tocas |
Data Cache + unstable_cache | 'use cache' + cacheLife + cacheTag |
ISR (next: { revalidate }) | cacheLife() con su perfil |
| Full Route Cache | El shell estático de PPR (prerenderizado) |
| Invalidación por tag | revalidateTag(tag, 'max') / updateTag(tag) |
| Router Cache (cliente) | Sigue existiendo, ya sin cachear dinámicas por default |
El gran cambio no es que haya nuevas APIs: es que el punto de control se movió. Antes cacheabas en los bordes (el fetch, la ruta, opciones dispersas); ahora cacheas en el centro (la función que produce el dato), con una sola directiva y un solo modelo de invalidación.
el árbol de decisión
Cuando dudes qué hacer con un dato, sigue esta cadena —es el mismo razonamiento que el equipo de Vercel recomienda:
- ¿El dato es específico de este usuario? (sesión, carrito, dashboard) → dinámico, dentro de
<Suspense>. No lo cachees. - Si no, ¿tolera estar un poco viejo? →
'use cache'. Si no lo tolera (tiene que ser exacto al segundo), déjalo dinámico. - ¿Cómo lo revalidas? Por horario →
cacheLifecon su perfil. Tras mutaciones →cacheTag+revalidateTag(oupdateTagsi el usuario debe ver su propio cambio al instante).
| Caso | Qué usar |
|---|---|
| Landing, blog, docs | Estático (default), nada que marcar |
| Catálogo, precios compartidos | 'use cache' + cacheLife + cacheTag |
| Dashboard, datos del usuario logueado | Dinámico + <Suspense> |
| Página mayormente estática con piezas personales | PPR (shell + dynamic holes) |
| Mutación que el autor debe ver ya | updateTag |
entonces, ¿qué hago hoy?
Si arrancas un proyecto nuevo en v16: activa cacheComponents: true y adopta el modelo mental de “dinámico por defecto, cacheo opt-in”. Es más simple de razonar porque elimina la magia: nada se cachea hasta que tú lo digas, y cuando lo dices, está a la vista en el código.
Si mantienes un proyecto en v14/v15: no tienes que migrar mañana. El modelo clásico —route segment config (export const dynamic, export const revalidate), generateStaticParams, opciones de fetch— sigue siendo válido mientras no enciendas el flag. Pero ten claro que es el modelo en retirada: bajo Cache Components, el route segment config para revalidación queda deprecado en favor de use cache.
Si vas a migrar: Next.js 16 pide Node 20.9+ y TypeScript 5.1+, y trae un codemod oficial que automatiza buena parte del trabajo mecánico (renombrar middleware a proxy, quitar los prefijos unstable_, etc.).
Y si tuviera que resumir la saga entera en una frase: el caching de Next.js pasó de “confía en que cacheamos bien por ti” a “tú dices qué cachear, y se ve”. Tres años de releases para aprender que, en algo tan cargado de trade-offs como la frescura de los datos, lo explícito le gana a lo mágico casi siempre.