Back

Por Qué Tu Caché de Next.js No Funciona (Y Cómo Arreglarlo en 2026)

¿Te ha pasado esto desarrollando con Next.js? "Actualicé los datos pero siguen mostrando lo mismo..." "Configuré revalidate pero no funciona..." "En local va perfecto pero en producción no..." Tranqui, no eres el único. El caché del App Router de Next.js es súper potente, pero también es la parte más confusa del framework.

En este artículo vamos a desglosar cada capa de caché en Next.js 15 y 16, entender por qué el caché se comporta raro a veces, y darte soluciones que realmente funcionan.

Las 4 capas de caché en Next.js (lo básico)

Antes de empezar a debugear, hay algo importante que debes saber: el App Router de Next.js no tiene UN caché, tiene 4 capas de caché independientes. Cada una hace algo diferente, y confundirlas es la raíz de la mayoría de los problemas.

1. Request Memoization (Memoización de Solicitudes)

Alcance: Pasada de renderizado única (solo del lado del servidor)
Duración: Duración de una única solicitud
Propósito: Deduplicar fetches de datos idénticos dentro de un único renderizado

// Ambas llamadas fetch se deduplican automáticamente // ¡Solo se hace UNA solicitud HTTP real! async function ProductDetails({ id }: { id: string }) { const product = await fetch(`/api/products/${id}`); return <ProductPrice productId={id} />; } async function ProductPrice({ productId }: { productId: string }) { // Este mismo fetch exacto se memoiza—sin solicitud duplicada const product = await fetch(`/api/products/${productId}`); return <span>${product.price}</span>; }

La memoización de solicitudes ocurre automáticamente para llamadas fetch con la misma URL y opciones durante un único renderizado del servidor. Este es el comportamiento nativo de React extendido por Next.js.

Concepto erróneo común: Esto NO es caché persistente. Una vez que la solicitud se completa, esta memoización desaparece. Solo previene fetches duplicados dentro del mismo recorrido del árbol de renderizado.

2. Data Cache (Caché de Datos)

Alcance: Del lado del servidor, persistente
Duración: Hasta revalidación o invalidación manual
Propósito: Cachear respuestas de fetch a través de solicitudes y despliegues

// Cacheado indefinidamente (comportamiento estático) const data = await fetch('https://api.example.com/data'); // Cacheado por 60 segundos const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 } }); // Nunca cacheado const data = await fetch('https://api.example.com/data', { cache: 'no-store' });

El Data Cache es donde ocurre la mayor parte de la confusión. Almacena la respuesta cruda de las llamadas fetch y persiste a través de:

  • Múltiples solicitudes de usuarios
  • Reinicios del servidor (en producción con la infraestructura adecuada)
  • Despliegues (en plataformas como Vercel)

3. Full Route Cache (Caché Completo de Ruta)

Alcance: Del lado del servidor, persistente
Duración: Hasta revalidación
Propósito: Cachear el HTML completo y el payload RSC (React Server Component) para las rutas

Cuando construyes una aplicación Next.js, las rutas estáticas se pre-renderizan en tiempo de construcción. El Full Route Cache almacena:

  • El HTML renderizado para la carga inicial de la página
  • El Payload RSC para la navegación del lado del cliente
// Esta página se genera estáticamente y se cachea completamente export default function AboutPage() { return <div>Sobre Nosotros</div>; } // Esta página opta por salir del Full Route Cache export const dynamic = 'force-dynamic'; export default function DashboardPage() { return <div>Dashboard: {new Date().toISOString()}</div>; }

4. Router Cache (Caché del Router, del lado del cliente)

Alcance: Del lado del cliente, por sesión
Duración: Basado en sesión con invalidación automática
Propósito: Cachear segmentos de ruta visitados en el navegador para navegación instantánea hacia atrás/adelante

Usuario visita /products → Router Cache almacena layout + página de /products Usuario navega a /products/123 → Router Cache almacena página de /products/123 → El layout de /products se reutiliza desde el caché Usuario hace clic en botón atrás → Página de /products servida instantáneamente desde Router Cache

El Router Cache cambió bastante en Next.js 15 y es lo que más confusión genera. Lo explicamos más abajo.

Cómo debugear cuando el caché no funciona

Cuando los datos no se actualizan, sigue estos pasos:

Paso 1: Identificar Qué Capa de Caché Está Involucrada

Hazte estas preguntas:

PreguntaSi es SíSi es No
¿Los datos desactualizados aparecen inmediatamente en la primera carga?Data Cache o Full Route CacheRouter Cache (lado del cliente)
¿La actualización forzada (Cmd+Shift+R) lo arregla?Router CacheCaché del lado del servidor
¿Redesplegar lo arregla?Full Route CacheMala configuración del Data Cache
¿Es en desarrollo?El servidor dev tiene caché diferenteVerificar build de producción

Paso 2: Verificar Tu Configuración de Fetch

El problema más común es malentender los valores predeterminados del caché de fetch.

// ❌ Error común: asumir que esto es dinámico const res = await fetch('https://api.example.com/user'); // ¡Esto está CACHEADO POR DEFECTO en Next.js! // ✅ Correcto: optar explícitamente por salir del caché const res = await fetch('https://api.example.com/user', { cache: 'no-store' }); // ✅ O usar revalidación basada en tiempo const res = await fetch('https://api.example.com/user', { next: { revalidate: 0 } // Revalidar en cada solicitud });

Paso 3: Entender la Configuración del Segmento de Ruta

La configuración del segmento de ruta afecta cómo se cachea toda la ruta:

// app/dashboard/page.tsx // Forzar que toda la ruta sea dinámica export const dynamic = 'force-dynamic'; // Forzar generación estática (dará error si se usan funciones dinámicas) export const dynamic = 'force-static'; // Establecer revalidación para toda la ruta export const revalidate = 60; // Revalidar cada 60 segundos // Deshabilitar todo el caché export const revalidate = 0; export const fetchCache = 'force-no-store';

Trampas que no están en la documentación

Trampa #1: cookies() y headers() Optan Automáticamente por Salir

Cuando usas funciones dinámicas en un Server Component, toda la ruta se vuelve dinámica:

import { cookies } from 'next/headers'; export default async function UserPage() { // ¡Solo llamar a cookies() hace esta ruta dinámica, // incluso si no usas el resultado! const cookieStore = await cookies(); // Este fetch ahora también es dinámico const user = await fetch('/api/user'); return <div>{user.name}</div>; }

La Solución: Si necesitas cookies pero quieres caché, reestructura tu componente:

// Separar las partes dinámicas y estáticas import { Suspense } from 'react'; export default function UserPage() { return ( <div> <StaticHeader /> {/* Esta parte está cacheada */} <Suspense fallback={<Loading />}> <DynamicUserContent /> {/* Solo esto es dinámico */} </Suspense> </div> ); }

Trampa #2: La Paradoja de searchParams

Acceder a searchParams en una página la hace dinámica, incluso si no usas parámetros de URL:

// ❌ Esta página es dinámica incluso si no se pasan search params export default function ProductsPage({ searchParams, }: { searchParams: { sort?: string }; }) { // Solo la presencia de searchParams en props = dinámico return <ProductGrid />; } // ✅ Mejor: solo acceder a searchParams cuando sea necesario export default function ProductsPage() { return <ProductGrid />; // Estático por defecto } // Para vistas filtradas, usar una ruta separada o componente cliente

Trampa #3: Solicitudes POST y Data Cache

Las solicitudes POST tienen un comportamiento de caché único que confunde a muchos desarrolladores:

// Las solicitudes POST NO están cacheadas por defecto const res = await fetch('/api/data', { method: 'POST', body: JSON.stringify({ id: 1 }), }); // ¡Pero si la respuesta usa headers de caché, PUEDE estar cacheada! // ¡Verifica los headers de respuesta de tu API!

Trampa #4: La Discrepancia Desarrollo vs Producción

Esta es quizás la trampa más frustrante. En desarrollo:

  • El Data Cache está deshabilitado por defecto
  • El Full Route Cache no existe
  • El hot reload a veces limpia los cachés inesperadamente
# Siempre prueba el comportamiento del caché con un build de producción npm run build && npm start

Si tus problemas de caché solo aparecen en producción, esta es la razón.

Trampa #5: Timing de ISR y revalidate

revalidate no significa "refrescar cada N segundos"—significa "después de N segundos, la PRÓXIMA solicitud disparará una regeneración en background":

export const revalidate = 60; // Línea de tiempo: // t=0: Página generada, cacheada // t=30: Usuario visita → obtiene versión cacheada (30s de antigüedad) // t=65: Usuario visita → obtiene versión cacheada (65s de antigüedad), dispara regeneración en background // t=66: Nuevo caché listo // t=70: Usuario visita → obtiene versión fresca

El patrón stale-while-revalidate significa que los usuarios podrían ver contenido desactualizado incluso después del período de revalidación.

Soluciones listas para copiar y pegar

Receta 1: Datos de Dashboard en Tiempo Real

Problema: El dashboard muestra datos desactualizados incluso después de actualizaciones.

// app/dashboard/page.tsx import { unstable_noStore as noStore } from 'next/cache'; export default async function Dashboard() { noStore(); // Optar por salir de todo el caché para este componente const stats = await fetchDashboardStats(); return <DashboardView stats={stats} />; } // O usar config de segmento de ruta export const dynamic = 'force-dynamic'; export const revalidate = 0;

Receta 2: Contenido Específico de Usuario con Layout Compartido

Problema: El layout está cacheado, pero el contenido de la página necesita ser específico del usuario.

// app/dashboard/layout.tsx // Este layout puede permanecer estático export default function DashboardLayout({ children }) { return ( <div className="dashboard-container"> <Sidebar /> {/* Cacheado */} {children} </div> ); } // app/dashboard/page.tsx import { cookies } from 'next/headers'; export default async function DashboardPage() { const cookieStore = await cookies(); const userId = cookieStore.get('userId')?.value; // Esta página es dinámica, pero el layout sigue cacheado const userData = await fetch(`/api/users/${userId}`, { cache: 'no-store' }); return <UserDashboard data={userData} />; }

Receta 3: Páginas de Productos E-Commerce con Actualizaciones de Precio

Problema: Los cambios de precio necesitan reflejarse en 5 minutos, pero las descripciones de productos pueden cachearse más tiempo.

// app/products/[id]/page.tsx export const revalidate = 300; // 5 minutos export default async function ProductPage({ params }) { // Los detalles del producto se revalidan cada 5 minutos const product = await fetch(`/api/products/${params.id}`, { next: { revalidate: 300 } }); // El inventario debería ser en tiempo real const inventory = await fetch(`/api/inventory/${params.id}`, { cache: 'no-store' }); return <ProductView product={product} inventory={inventory} />; }

Receta 4: Blog con Revalidación Bajo Demanda

Problema: Las publicaciones del blog deberían estar cacheadas, pero revalidarse cuando el contenido se actualiza en el CMS.

// app/blog/[slug]/page.tsx export const revalidate = false; // Cachear indefinidamente export default async function BlogPost({ params }) { const post = await fetch(`/api/posts/${params.slug}`, { next: { tags: [`post-${params.slug}`] } }); return <Article post={post} />; } // app/api/revalidate/route.ts import { revalidateTag } from 'next/cache'; import { NextRequest } from 'next/server'; export async function POST(request: NextRequest) { const { slug, secret } = await request.json(); if (secret !== process.env.REVALIDATION_SECRET) { return Response.json({ error: 'Secret inválido' }, { status: 401 }); } revalidateTag(`post-${slug}`); return Response.json({ revalidated: true }); }

Receta 5: Arreglando Problemas del Router Cache (Next.js 15)

Problema: La navegación del lado del cliente muestra datos desactualizados.

Desde Next.js 15, el comportamiento del Router Cache cambió significativamente. Las páginas dinámicas ya no se cachean en el cliente por defecto, pero las páginas estáticas sí. Next.js 16 continúa este enfoque con mejoras adicionales.

// Para rutas dinámicas que necesitan datos frescos en cada navegación: import { useRouter } from 'next/navigation'; function RefreshButton() { const router = useRouter(); const handleRefresh = () => { router.refresh(); // Invalida el Router Cache para la ruta actual }; return <button onClick={handleRefresh}>Actualizar Datos</button>; }

Para un cache busting más agresivo:

// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const response = NextResponse.next(); // Prevenir Router Cache para rutas específicas if (request.nextUrl.pathname.startsWith('/dashboard')) { response.headers.set( 'Cache-Control', 'no-store, must-revalidate' ); } return response; }

Patrones Avanzados: Estrategias de Cache Tags

Los cache tags te permiten crear relaciones entre datos cacheados e invalidarlos con precisión:

// lib/data.ts export async function getProducts(category: string) { return fetch(`/api/products?category=${category}`, { next: { tags: [ 'products', // Todos los productos `category-${category}`, // Categoría específica ] } }); } export async function getProductById(id: string) { return fetch(`/api/products/${id}`, { next: { tags: [ 'products', `product-${id}`, ] } }); } // Cuando un solo producto se actualiza: revalidateTag(`product-${updatedProductId}`); // Cuando la categoría se reorganiza: revalidateTag(`category-${categoryName}`); // Opción nuclear - todos los productos: revalidateTag('products');

Herramientas y Técnicas de Depuración

1. Inspección de Headers de Caché

// Agregar headers de debug de caché en desarrollo // next.config.js module.exports = { async headers() { return [ { source: '/(.*)', headers: [ { key: 'x-next-cache-status', value: process.env.NODE_ENV === 'development' ? 'disabled' : 'enabled', }, ], }, ]; }, };

2. Logging del Comportamiento del Caché

// lib/fetch-with-logging.ts export async function fetchWithCacheLogging(url: string, options?: RequestInit) { const startTime = performance.now(); const response = await fetch(url, options); console.log({ url, cacheMode: options?.cache ?? 'default', revalidate: (options as any)?.next?.revalidate ?? 'no establecido', responseTime: `${(performance.now() - startTime).toFixed(2)}ms`, fromCache: response.headers.get('x-cache') === 'HIT', }); return response; }

3. Indicador Visual de Caché

// components/CacheDebugBadge.tsx import { unstable_cache } from 'next/cache'; export function CacheDebugBadge() { if (process.env.NODE_ENV !== 'development') return null; const renderTime = new Date().toISOString(); return ( <div className="fixed bottom-4 right-4 bg-black text-white p-2 text-xs rounded"> Renderizado: {renderTime} </div> ); }

Cambios de Caché en Next.js 15/16: ¿Qué Hay de Nuevo?

Next.js 15 introdujo varios cambios importantes en los valores predeterminados del caché, y Next.js 16 (lanzado en octubre de 2025) revoluciona el caché con un enfoque completamente nuevo.

Cambios en Next.js 15 (Todavía Relevantes)

1. Las Solicitudes fetch Ya No Se Cachean por Defecto

// Next.js 14 y anteriores: cacheado por defecto // Next.js 15+: NO cacheado por defecto // Para restaurar el comportamiento anterior: const data = await fetch(url, { cache: 'force-cache' });

2. Los Route Handlers Son Dinámicos por Defecto

// Next.js 14 y anteriores: los handlers GET estaban cacheados // Next.js 15+: todos los handlers son dinámicos por defecto // Para hacer un route handler estático: export const dynamic = 'force-static'; export async function GET() { return Response.json({ time: new Date().toISOString() }); }

3. Cambios en el Router Cache del Cliente

// Next.js 14 y anteriores: Páginas cacheadas por 30 segundos (dinámico) o 5 minutos (estático) // Next.js 15+: Páginas dinámicas tienen 0 tiempo de staleness // Para configurar el tiempo de staleness: // next.config.js module.exports = { experimental: { staleTimes: { dynamic: 30, // segundos static: 180, // segundos }, }, };

Next.js 16: La Revolución de "use cache"

Next.js 16 introduce Cache Components con la directiva "use cache"—el cambio de caché más significativo desde que se introdujo el App Router. Esto completa la visión de Partial Pre-Rendering (PPR).

El Cambio de Paradigma: De Implícito a Explícito

En Next.js 16, todo el código dinámico se ejecuta en tiempo de solicitud por defecto. El caché es ahora completamente opt-in:

// Next.js 16: Esta página es dinámica por defecto export default async function ProductPage({ params }) { const product = await fetch(`/api/products/${params.id}`); return <ProductView product={product} />; } // Para cachear, DEBES usar "use cache" explícitamente "use cache" export default async function ProductPage({ params }) { const product = await fetch(`/api/products/${params.id}`); return <ProductView product={product} />; }

Usando "use cache" en Diferentes Niveles

// Cachear una página completa "use cache" export default async function BlogPostPage({ params }) { const post = await fetchPost(params.slug); return <Article post={post} />; } // Cachear un componente específico async function CachedSidebar() { "use cache" const categories = await fetchCategories(); return <Sidebar categories={categories} />; } // Cachear una función async function getExpensiveData(id: string) { "use cache" // Este resultado será cacheado return await computeExpensiveData(id); }

Combinando con Cache Tags

"use cache" import { cacheTag } from 'next/cache'; export default async function ProductPage({ params }) { cacheTag(`product-${params.id}`); const product = await fetch(`/api/products/${params.id}`); return <ProductView product={product} />; } // Invalidar con revalidateTag import { revalidateTag } from 'next/cache'; revalidateTag(`product-${params.id}`);

Nuevo: cacheLife para Control de Expiración

"use cache" import { cacheLife } from 'next/cache'; export default async function DashboardStats() { cacheLife('minutes'); // Perfiles predefinidos: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max' const stats = await fetchStats(); return <StatsView stats={stats} />; } // O con valores personalizados cacheLife({ stale: 60, // Servir stale durante 60 segundos revalidate: 300, // Revalidar después de 5 minutos expire: 3600, // Expirar después de 1 hora });

Caché del Sistema de Archivos Turbopack (Beta)

Next.js 16 hace de Turbopack el bundler por defecto con caché del sistema de archivos:

// next.config.js module.exports = { experimental: { turbo: { // Caché del sistema de archivos para rebuilds más rápidos persistentCaching: true, }, }, };

Esto mejora dramáticamente los tiempos de inicio y compilación al almacenar artefactos del compilador en disco entre ejecuciones.

Implicaciones de Rendimiento: Cuándo Cachear Qué

Entender el caché no es solo evitar bugs—también se trata de optimizar el rendimiento:

Tipo de ContenidoEstrategia RecomendadaRazón
Páginas de marketingrevalidate: 3600 o estático en buildCambia raramente, maximiza hits de CDN
Listados de e-commercerevalidate: 60 + bajo demandaEquilibrio entre frescura y rendimiento
Dashboards de usuariodynamic = 'force-dynamic'Específico del usuario, necesita datos frescos
Rutas API (públicas)revalidate: 300 + cache tagsReducir carga del backend
Rutas API (auth)cache: 'no-store'Seguridad y frescura
Datos en tiempo realFetching del lado del clienteCaché del servidor inapropiado

Mensajes de Error Comunes y Soluciones

"DYNAMIC_SERVER_USAGE"

Error: Dynamic server usage: cookies

Solución: Estás usando funciones dinámicas en una ruta de exports estáticos. Puedes:

  • Eliminar la función dinámica
  • Agregar export const dynamic = 'force-dynamic'
  • Mover la lógica dinámica a componentes cliente

"Invariant: static generation store missing"

Esto usualmente indica intentar usar APIs dinámicas durante el build:

// ❌ Problema export async function generateStaticParams() { const cookieStore = await cookies(); // ¡No puedes usar esto aquí! return []; } // ✅ Solución: solo usar datos estáticos en generateStaticParams export async function generateStaticParams() { const products = await fetch('/api/products', { cache: 'force-cache' }).then(r => r.json()); return products.map(p => ({ id: p.id })); }

El Caché No Se Invalida en Vercel

// Asegúrate de estar usando la API de revalidación correcta import { revalidatePath, revalidateTag } from 'next/cache'; // revalidatePath invalida todos los datos para una ruta revalidatePath('/products'); // revalidateTag es más quirúrgico revalidateTag('products-list'); // Ambos requieren que la solicitud se origine desde el mismo despliegue // Usar Webhook desde CMS → API Route → función revalidate

Conclusión: Modelo Mental para el Caché de Next.js

Después de digerir todo esto, aquí está el modelo mental para llevar adelante:

  1. Por defecto a dinámico en Next.js 15/16. Comienza sin caché y agrégalo donde sea beneficioso, en lugar de depurar hits de caché inesperados.

  2. Separa lo estático y lo dinámico. Usa límites Suspense y composición de componentes para maximizar el contenido cacheable.

  3. Cachea en el nivel correcto. No uses Full Route Cache cuando el Data Cache para fetches específicos sería suficiente.

  4. Siempre prueba con builds de producción. El servidor de desarrollo miente sobre el comportamiento del caché.

  5. Usa cache tags para precisión. Son más mantenibles que la invalidación basada en rutas para apps complejas.

  6. Monitorea el comportamiento del caché en producción. Agrega logging, usa Vercel Analytics, o implementa headers de caché personalizados.

El caché en Next.js es complejo porque está resolviendo un problema complejo: entregar contenido rápido y fresco a escala. Una vez que entiendas las cuatro capas y sus interacciones, podrás construir aplicaciones que sean increíblemente rápidas y confiablemente actualizadas.

La clave no es luchar contra el caché, sino trabajar con él—configurando cada capa apropiadamente para tu caso de uso específico y entendiendo que a veces, la mejor configuración de caché es no tener caché en absoluto.

nextjscachingapp-routerreactweb-performancetroubleshooting

Explora herramientas relacionadas

Prueba estas herramientas gratuitas de Pockit