Next.js 16 cacheComponents: Migrando 91,000 páginas desde App Router Caching
Habíamos estado ejecutando un gran catálogo de comercio electrónico en Next.js 14's App Router durante aproximadamente dieciocho meses cuando se lanzó Next.js 16. 91,247 páginas. Listados de productos, árboles de categorías, contenido editorial, variantes localizadas en 14 mercados. El modelo de almacenamiento en caché antiguo -- donde los Server Components se almacenaban en caché por defecto -- se había convertido en un campo minado de errores de datos obsoletos y spaghetti de revalidateTag. Cuando el equipo de Next.js anunció cacheComponents y el cambio a no-caching-por-defecto en Next.js 15 (llevado adelante y refinado en v16), supimos que era el momento. Esta es la historia de esa migración: qué funcionó, qué no funcionó, y los números de rendimiento del otro lado.
Tabla de Contenidos
- El Problema de Almacenamiento en Caché que Realmente Teníamos
- Qué Cambió en Next.js 15 y 16
- Entendiendo cacheComponents
- Nuestra Estrategia de Migración para 91,000 Páginas
- Implementación: Paso a Paso
- Resultados de Rendimiento y Benchmarks
- Trampas y Dificultades
- Cuándo Usar y Cuándo No Usar cacheComponents
- Preguntas Frecuentes

El Problema de Almacenamiento en Caché que Realmente Teníamos
Permíteme pintar el cuadro. En Next.js 14's App Router, las solicitudes de fetch en Server Components se almacenaban en caché por defecto. El Data Cache persistía entre despliegues. El Full Route Cache almacenaba HTML renderizado y cargas útiles de RSC en tiempo de compilación. Y el Router Cache en el lado del cliente mantenía segmentos precargados durante... bueno, más tiempo del esperado.
Para un sitio con 91,000 páginas, este enfoque de caché-por-defecto-para-todo creó dos categorías de problemas:
Datos obsoletos en todas partes. Los precios de los productos se actualizaban en nuestro CMS sin interfaz (Sanity, en nuestro caso) pero los resultados de fetch almacenados en caché se quedaban. Teníamos llamadas de revalidateTag dispersas en 47 acciones de servidor diferentes. ¿Perder una etiqueta? Un cliente ve el precio de ayer. Literalmente teníamos un canal de Slack llamado #cache-crimes donde el equipo de contenido reportaba páginas obsoletas.
Tiempos de compilación del infierno. La generación estática completa de 91,000 páginas tardaba más de 3 horas. Habíamos pasado a ISR con revalidate: 3600 para la mayoría de las páginas, pero la interacción entre ISR, el Data Cache y la revalidación bajo demanda era genuinamente difícil de razonar. Los nuevos desarrolladores del equipo pasarían sus primeras dos semanas simplemente entendiendo las capas de almacenamiento en caché.
El Costo del Modelo Mental
Aquí está lo que creo que la gente subestima: el costo cognitivo del almacenamiento en caché implícito. Cuando el almacenamiento en caché es el predeterminado y optas por no hacerlo, cada nuevo componente te requiere preguntar "¿debería esto estar en caché?" y luego recordar agregar la directiva correcta si la respuesta es no. Cuando el no-caching es el predeterminado y optas por hacerlo, solo piensas en almacenamiento en caché cuando activamente lo deseas. Ese es un modelo fundamentalmente diferente -- y mejor.
Qué Cambió en Next.js 15 y 16
Next.js 15 fue el gran cambio filosófico. El equipo invirtió los predeterminados:
| Comportamiento | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
fetch() en Server Components |
En caché por defecto | No en caché por defecto | No en caché por defecto |
| Route Handlers (GET) | En caché por defecto | No en caché por defecto | No en caché por defecto |
| Client Router Cache | 30s (dinámico) / 5min (estático) | 0s para segmentos de página | 0s predeterminado, configurable |
| Full Route Cache | Habilitado para rutas estáticas | Mismo | Mismo, con refinamientos de cacheLife |
| Almacenamiento en caché a nivel de componente | unstable_cache |
directiva use cache (experimental) |
API cacheComponents (estable) |
Next.js 15 introdujo la directiva use cache como una característica experimental detrás de una bandera. Next.js 16, lanzado a principios de 2025, estabilizó esto como la opción de configuración cacheComponents y la directiva "use cache" asociada, junto con cacheLife para definir perfiles de caché personalizados e cacheTag para invalidación dirigida.
La idea clave: el almacenamiento en caché se trasladó de ser un comportamiento implícito del framework a una opción explícita del desarrollador a nivel de componente. Esto es enorme para sitios grandes.
Entendiendo cacheComponents
La característica cacheComponents en next.config.js habilita el almacenamiento en caché a nivel de componente a través de la directiva "use cache". Aquí está la configuración básica:
// next.config.js (Next.js 16)
const nextConfig = {
experimental: {
cacheComponents: true,
},
};
module.exports = nextConfig;
Una vez habilitado, puedes agregar "use cache" en la parte superior de cualquier Server Component asincrónico, acción de servidor, o incluso un archivo de layout:
// app/products/[slug]/page.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export default async function ProductPage({ params }: { params: { slug: string } }) {
cacheLife('products'); // perfil de caché personalizado
cacheTag(`product-${params.slug}`);
const product = await fetchProduct(params.slug);
return (
<div>
<h1>{product.name}</h1>
<ProductDetails product={product} />
<DynamicPricing productId={product.id} /> {/* Este componente NO está en caché */}
</div>
);
}
Perfiles cacheLife
Aquí es donde se pone interesante para sitios grandes. Defines perfiles de caché nombrados en next.config.js:
const nextConfig = {
experimental: {
cacheComponents: true,
cacheLife: {
products: {
stale: 300, // servir obsoleto durante 5 minutos
revalidate: 3600, // revalidar después de 1 hora
expire: 86400, // expiración dura después de 24 horas
},
editorial: {
stale: 3600,
revalidate: 86400,
expire: 604800, // 7 días
},
navigation: {
stale: 86400,
revalidate: 604800,
expire: 2592000, // 30 días
},
},
},
};
El modelo de tres niveles (stale, revalidate, expire) se mapea bien a la semántica stale-while-revalidate. Durante la ventana de stale, el contenido en caché se sirve inmediatamente. Después de stale pero antes de expire, una revalidación en segundo plano se activa. Después de expire, la entrada de caché se ha ido.
cacheTag para Invalidación
La función cacheTag reemplaza el patrón antiguo de revalidateTag con algo más componible:
import { revalidateTag } from 'next/cache';
// En un manejador de webhook o acción de servidor:
export async function handleProductUpdate(productSlug: string) {
revalidateTag(`product-${productSlug}`);
revalidateTag('product-listing'); // invalidar páginas de listado también
}
Esta parte no cambió mucho desde Next.js 15, pero funciona mucho mejor con cacheComponents porque estás etiquetando componentes específicos en caché en lugar de intentar invalidar cachés opacos a nivel de framework.

Nuestra Estrategia de Migración para 91,000 Páginas
No lo hicimos de una sola vez. Con 91,000 páginas en 14 locales, una migración de gran explosión habría sido imprudente. Así es como lo dividimos:
Fase 1: Actualizar a Next.js 16, Sin Cambios de Caché (Semana 1-2)
Actualizamos de Next.js 14.2 a 16.0 sin habilitar cacheComponents. Solo esto cambió el comportamiento porque las solicitudes de fetch ya no se almacenaban en caché por defecto. Esperábamos regresiones de TTFB y las obtuvimos:
- El TTFB promedio pasó de 180ms a 340ms en páginas de productos
- La carga del servidor de origen aumentó aproximadamente un 60% (nuestro CDN de Sanity se mantuvo bien, pero nuestros puntos finales de API personalizados no)
- La revalidación de ISR en realidad se hizo más rápida porque había menos estado de caché que administrar
Esto confirmó lo que sospechábamos: habíamos estado confiando mucho en almacenamiento en caché implícito, y muchas de nuestras páginas genuinamente necesitaban almacenamiento en caché -- solo que explícito e intencional.
Fase 2: Auditar y Clasificar Páginas (Semana 3)
Categorizamos cada ruta en nuestra aplicación:
| Tipo de Página | Cantidad | Estrategia de Caché | Perfil cacheLife |
|---|---|---|---|
| Páginas de detalle de producto | 42,000 | Caché con etiqueta de producto | products (5min obsoleto / 1hr revalidar) |
| Páginas de listado de categorías | 3,200 | Caché con etiqueta de categoría | products (5min obsoleto / 1hr revalidar) |
| Páginas editoriales/blog | 8,400 | Caché agresivamente | editorial (1hr obsoleto / 24hr revalidar) |
| Variantes localizadas | 31,647 | Igual que la página base | Heredado de la base |
| Páginas de cuenta/dinámicas | 6,000 | Sin caché | N/A |
Fase 3: Habilitar cacheComponents, Agregar Directivas (Semana 4-6)
Habilitamos la bandera y comenzamos a agregar directivas "use cache". La decisión clave: almacenamos en caché a nivel de página para la mayoría de las rutas, pero a nivel de componente para páginas con contenido mixto estático/dinámico.
Para páginas de productos, la información del producto e imágenes se almacenaban en caché, pero el componente de precios y el estado del inventario se dejaban sin caché:
// components/ProductInfo.tsx
"use cache";
import { cacheLife, cacheTag } from 'next/cache';
export async function ProductInfo({ slug }: { slug: string }) {
cacheLife('products');
cacheTag(`product-${slug}`, 'product-info');
const product = await getProduct(slug);
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<ProductImages images={product.images} />
</section>
);
}
// components/DynamicPricing.tsx
// SIN directiva "use cache" -- siempre fresco
export async function DynamicPricing({ productId }: { productId: string }) {
const pricing = await getPricing(productId); // toca la API de precios en cada solicitud
return (
<div className="pricing">
<span className="price">${pricing.current}</span>
{pricing.onSale && <span className="was-price">${pricing.original}</span>}
</div>
);
}
Fase 4: Integración de Webhooks (Semana 7)
Recableamos nuestros webhooks de Sanity para llamar a revalidateTag con las etiquetas correctas. Esto fue en realidad más simple que nuestra configuración anterior porque las etiquetas ahora eran explícitas en el código, no dispersas en las opciones de fetch.
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const body = await request.json();
const secret = request.headers.get('x-webhook-secret');
if (secret !== process.env.REVALIDATION_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
switch (body._type) {
case 'product':
revalidateTag(`product-${body.slug.current}`);
revalidateTag('product-listing');
break;
case 'category':
revalidateTag(`category-${body.slug.current}`);
revalidateTag('navigation');
break;
case 'article':
revalidateTag(`article-${body.slug.current}`);
break;
}
return new Response('OK', { status: 200 });
}
Implementación: Paso a Paso
Si estás haciendo una migración similar, aquí está el manual práctico que recomendaríamos (y lo que ahora usamos para proyectos de desarrollo Next.js en Social Animal):
Paso 1: Habilitar la Bandera
// next.config.js
module.exports = {
experimental: {
cacheComponents: true,
cacheLife: {
// Comienza con predeterminados sensatos
default: {
stale: 60,
revalidate: 900,
expire: 86400,
},
},
},
};
Paso 2: Encuentra Tus Rutas Calientes
Usa tu análisis para identificar las páginas que obtienen más tráfico y dónde TTFB importa más. Para nosotros, fueron las páginas de categorías (alto tráfico, contenido relativamente estable) y páginas de productos (alto tráfico, contenido moderadamente dinámico).
Paso 3: Agregar `"use cache"` de Arriba Hacia Abajo
Comienza con layouts. Si tu layout raíz obtiene datos de navegación, almacena eso en caché primero -- es el cambio de mayor impacto y menor riesgo:
// app/layout.tsx
// Nota: "use cache" en layouts almacena en caché el shell del layout
// Las páginas secundarias aún se renderizan independientemente
import { Navigation } from '@/components/Navigation';
export default async function RootLayout({ children }) {
return (
<html>
<body>
<Navigation /> {/* Este componente tiene su propio "use cache" */}
{children}
</body>
</html>
);
}
Paso 4: Configurar Monitoreo
Usamos la analítica incorporada de Vercel más registro personalizado para rastrear tasas de acierto de caché. En la primera semana después de habilitar cacheComponents, nuestra tasa de acierto de caché fue solo del 34%. Después de ajustar duraciones de stale, subió al 78%.
Resultados de Rendimiento y Benchmarks
Aquí están los números reales después de la migración completa, medidos durante un período de 30 días en el plan Pro de Vercel:
| Métrica | Antes (Next.js 14) | Después Fase 1 (v16, sin caché) | Después Migración Completa |
|---|---|---|---|
| TTFB promedio (páginas de productos) | 180ms | 340ms | 95ms |
| TTFB promedio (páginas de categorías) | 220ms | 410ms | 72ms |
| TTFB promedio (páginas editoriales) | 150ms | 280ms | 45ms |
| P99 TTFB (todas las páginas) | 1,200ms | 2,100ms | 380ms |
| Tiempo de compilación (completo) | 3h 12min | 2h 48min | 48min |
| Invocaciones de función de Vercel/día | 2.4M | 3.8M | 1.1M |
| Factura mensual de Vercel | ~$840 | ~$1,200 | ~$520 |
| Tasa de acierto de caché | Desconocida (implícita) | N/A | 78% |
| Incidentes de contenido obsoleto (#cache-crimes) | 8-12/semana | 0 | 1-2/mes |
La mejora del tiempo de compilación merece explicación. Con cacheComponents, nos alejamos de generar todas las 91,000 páginas en tiempo de compilación. En su lugar, generamos estáticamente solo las 5,000 páginas principales (por tráfico) y dejamos que el resto se genere bajo demanda con almacenamiento en caché. La directiva cacheComponents significaba que esas páginas generadas bajo demanda se almacenaban en caché después de la primera visita, con cacheLife controlando la obsolescencia.
La caída de la factura de Vercel fue significativa. Menos invocaciones de función (debido al almacenamiento en caché de componentes explícito) más tiempos de compilación más cortos significaron ahorros de costos reales. Esa reducción de ~$320/mes se amortiza a sí misma.
Trampas y Dificultades
Límites de Serialización
La directiva "use cache" crea un límite de serialización. Todo lo que se pasa a un componente en caché como props debe ser serializable. Teníamos varios componentes que recibían funciones de devolución de llamada o elementos React como props -- esos se rompieron inmediatamente. La solución fue reestructurar para usar patrones de composición en su lugar:
// ❌ Esto se rompe con "use cache"
"use cache";
export async function ProductCard({ product, onAddToCart }) {
// onAddToCart es una función -- ¡no serializable!
}
// ✅ Esto funciona
"use cache";
export async function ProductCard({ product }) {
return (
<div>
<h2>{product.name}</h2>
{/* AddToCart es un Client Component, no almacenado en caché */}
<AddToCartButton productId={product.id} />
</div>
);
}
Parámetros Dinámicos y Explosión de Clave de Caché
Con 91,000 páginas, cada una con parámetros únicos, el espacio de clave de caché es enorme. Golpeamos los límites de caché edge de Vercel en la primera semana y tuvimos que ser más estratégicos sobre qué páginas obtuvieron valores de expire largos. Las páginas de cola larga de bajo tráfico obtuvieron duraciones de caché más cortas.
La Trampa de `Date.now()`
Cualquier componente que use "use cache" que llame a Date.now() o new Date() dentro de la función en caché almacenará en caché esa marca de tiempo. Encontramos esto en una pantalla de "última actualización" que mostraba la misma hora durante horas. La solución: mover la lógica sensible al tiempo a un Client Component o un Server Component sin caché.
Límites de Caché Anidados
Cuando anidas componentes en caché dentro de otros componentes en caché, el caché interno tiene su propio ciclo de vida. Esto es poderoso pero confuso. Establecimos una convención del equipo: almacenar en caché a nivel de página O a nivel de componente, no ambos, a menos que haya una razón clara.
Cuándo Usar y Cuándo No Usar cacheComponents
Úsalo cuando:
- Tienes más de unos pocos cientos de páginas y los tiempos de compilación de ISR son dolorosos
- Tu contenido tiene requisitos claros de actualización que varían por sección
- Necesitas control granular sobre lo que está en caché versus siempre-fresco
- Estás ejecutándote en Vercel o una plataforma que admite las capas de caché de Next.js
- Quieres reducir costos de infraestructura en sitios de alto tráfico
No lo uses cuando:
- Tu sitio es lo suficientemente pequeño para que SSG completo funcione bien
- Cada página es totalmente dinámica (contenido específico del usuario en todas partes)
- No estás en una plataforma de hosting que admita infraestructura de caché de Next.js
- Tu equipo es nuevo en Next.js -- primero familiarízate con lo básico
Si estás evaluando si tu proyecto necesita este nivel de control de almacenamiento en caché, o si un framework diferente como Astro podría ser una mejor opción para tu sitio con mucho contenido, vale la pena pensar antes de comprometerse con una migración.
Para proyectos donde el contenido proviene de múltiples fuentes de CMS sin interfaz, el sistema cacheTag en Next.js 16 funciona hermosamente con arquitecturas de CMS sin interfaz -- cada tipo de contenido obtiene su propio canal de invalidación.
Preguntas Frecuentes
¿Qué es cacheComponents en Next.js 16?
cacheComponents es una opción de configuración experimental en Next.js 16 que habilita la directiva "use cache" para Server Components. Te permite marcar explícitamente qué componentes deben ser almacenados en caché y definir perfiles de caché personalizados usando cacheLife. Es la evolución estable de la directiva use cache que era experimental en Next.js 15.
¿Cómo es cacheComponents diferente de ISR (Regeneración Estática Incremental)?
ISR almacena en caché páginas completas y las revalida en un cronograma basado en tiempo. cacheComponents te permite almacenar en caché componentes individuales dentro de una página, cada uno con diferentes duraciones de caché. Una sola página puede tener un encabezado almacenado en caché durante 24 horas, información del producto almacenada en caché durante 1 hora, y precios que nunca se almacenan en caché. ISR no puede hacer eso -- es todo o nada a nivel de página.
¿Necesito estar en Vercel para usar cacheComponents?
No, pero la experiencia es mejor en Vercel porque la infraestructura de almacenamiento en caché está estrechamente integrada. Los despliegues autoalojados de Next.js pueden usar cacheComponents con el adaptador de caché del sistema de archivos, pero no obtendrás los beneficios de distribución edge. Plataformas como Netlify y Cloudflare están agregando soporte, pero a mediados de 2025, Vercel sigue siendo la implementación más completa.
¿Cómo invalido componentes almacenados en caché en Next.js 16?
Usas cacheTag() dentro de tu componente en caché para asignar etiquetas, luego llamas a revalidateTag('nombre-etiqueta') desde una acción de servidor, manejador de ruta o punto final de webhook. Esto invalida todos los componentes almacenados en caché con esa etiqueta. Es la misma API de Next.js 15, pero es más útil ahora porque estás etiquetando componentes específicos en caché en lugar de cachés opacos a nivel de framework.
¿Reducirá cacheComponents mi factura de Vercel? Puede reducir significativamente los costos. En nuestro caso, las invocaciones de función bajaron un 54% porque las respuestas de componentes en caché se sirvieron desde la capa de caché en lugar de invocar funciones sin servidor. La reducción del tiempo de compilación también ahorra en minutos de compilación. Tu experiencia variará según patrones de tráfico y tasas de acierto de caché -- verifica la calculadora de precios de Vercel con tu uso actual.
¿Qué sucede si agrego "use cache" a un componente que recibe props no serializables?
Obtendrás un error de compilación. La directiva "use cache" crea un límite de serialización, así que todos los props deben ser serializables (cadenas, números, objetos planos, arrays). Funciones, elementos React, instancias de clases y otros valores no serializables causarán que la compilación falle. Reestructura tu componente para aceptar solo props de datos y manejar interactividad en Client Components secundarios.
¿Puedo usar cacheComponents con React Server Components de otros frameworks?
No. cacheComponents es una característica específica de Next.js que se construye sobre React's Server Components. Aunque la sintaxis de la directiva "use cache" eventualmente podría convertirse en un estándar de React, los perfiles de cacheLife y el sistema cacheTag son APIs de Next.js. Si estás usando un framework como Remix o una configuración de RSC personalizada, necesitarás estrategias de almacenamiento en caché diferentes.
¿Cuánto tiempo lleva migrar un sitio grande de Next.js a cacheComponents? Para nuestro sitio de 91,000 páginas con un equipo de 4 desarrolladores, la migración completa tardó 7 semanas incluyendo pruebas y monitoreo. Un sitio más pequeño (menos de 10,000 páginas) con un modelo de datos más simple probablemente podría hacerlo en 1-2 semanas. Los cambios de código reales son sencillos -- el tiempo se dedica a auditar tus necesidades de almacenamiento en caché, probar flujos de invalidación y monitorear tasas de acierto de caché después del despliegue. Si prefieres no hacerlo solo, contáctanos -- hemos hecho esto algunas veces ya.