Next.js 16 cacheComponents: Migración de 91,000 páginas sin tiempo de inactividad
Tu deploy se envía a las 2:14 AM. Noventa y un mil páginas de productos—árboles de categorías, variantes localizadas en 14 mercados, contenido SEO editorial—cambian de Next.js 14's App Router al API cacheComponents de v16. Observas el primer cascada de solicitudes en los logs de Vercel. TTFB se dispara a 1.8 segundos. Tu Slack pita. Durante dieciocho meses habías lidiado con el modelo de caché predeterminado antiguo: precios obsoletos, llamadas revalidateTag que se ejecutaban demasiado tarde, tickets de atención al cliente culpando "al sitio web". Next.js 15 desactivó el almacenamiento en caché de forma predeterminada; v16 te dio cacheComponents para optar de vuelta quirúrgicamente. Elegiste migración sobre una muerte lenta por mil bugs de caché. Ahora estás mirando tráfico real, errores reales, e un incidente de producción dos horas antes del amanecer. Aquí está lo que se rompió, lo que resistió, y los deltas de rendimiento que ningún conjunto de benchmarks predijo.
Tabla de contenidos
- El problema de 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 problemas
- Cuándo deberías y no deberías usar cacheComponents
- Preguntas frecuentes

El problema de caché que realmente teníamos
Déjame pintar el cuadro. En Next.js 14's App Router, las solicitudes fetch en Server Components se almacenaban en caché de forma predeterminada. El Data Cache persistía en los deployments. El Full Route Cache almacenaba el HTML renderizado y cargas útiles RSC en el momento de la compilación. Y el Router Cache en el lado del cliente mantenía segmentos prefetched durante... bueno, más tiempo de lo esperado.
Para un sitio con 91,000 páginas, este enfoque predeterminado de caché-todo creó dos categorías de problemas:
Datos obsoletos en todas partes. Los precios de productos se actualizaban en nuestro CMS headless (Sanity, en nuestro caso) pero los resultados de fetch almacenados en caché se quedaban. Teníamos llamadas revalidateTag esparcidas 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 migrado 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 en el 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 te excluyes, cada nuevo componente requiere que te preguntes "¿debería estar en caché?" y luego recuerdes agregar la directiva correcta si la respuesta es no. Cuando el no-almacenamiento en caché es el predeterminado y optas, solo piensas en caché cuando realmente lo quieres. 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 valores predeterminados:
| Comportamiento | Next.js 14 | Next.js 15 | Next.js 16 |
|---|---|---|---|
fetch() en Server Components |
En caché de forma predeterminada | No en caché de forma predeterminada | No en caché de forma predeterminada |
| Route Handlers (GET) | En caché de forma predeterminada | No en caché de forma predeterminada | No en caché de forma predeterminada |
| 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 | Igual | Igual, con refinamientos cacheLife |
| Almacenamiento en caché a nivel de componente | unstable_cache |
use cache directiva (experimental) |
cacheComponents API (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 asociada "use cache", junto con cacheLife para definir perfiles de caché personalizados y cacheTag para invalidación dirigida.
La idea clave: el almacenamiento en caché se movió de ser un comportamiento implícito del marco 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 async, server action, 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'); // custom cache profile
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 semántica stale-while-revalidate. Durante la ventana stale, el contenido en caché se sirve inmediatamente. Después de stale pero antes de expire, comienza la revalidación de fondo. 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 server action:
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 en caché explícitos en lugar de intentar invalidar cachés opacos a nivel de marco.

Nuestra estrategia de migración para 91,000 páginas
No hicimos esto de un golpe. Con 91,000 páginas en 14 locales, una migración big-bang hubiera sido imprudente. Así es cómo 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 fetch ya no se almacenaban en caché de forma predeterminada. Esperábamos regresiones 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 API personalizados no)
- La revalidación ISR en realidad se aceleró porque había menos estado de caché para administrar
Esto confirmó lo que sospechábamos: habíamos estado confiando mucho en el 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ía | 3,200 | Caché con etiqueta de categoría | products (5min obsoleto / 1hr revalidar) |
| Páginas editoriales/blog | 8,400 | Caché agresivo | editorial (1hr obsoleto / 24hr revalidar) |
| Variantes localizadas | 31,647 | Igual que página base | Heredado de 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 y las imágenes estaban 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); // accede a 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 webhook (Semana 7)
Recableamos nuestros webhooks de Sanity para llamar revalidateTag con las etiquetas correctas. Esto fue realmente más simple que nuestra configuración anterior porque las etiquetas ahora eran explícitas en el código, no esparcidas en 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: {
// Comenzar con valores predeterminados sensatos
default: {
stale: 60,
revalidate: 900,
expire: 86400,
},
},
},
};
Paso 2: Encontrar tus rutas críticas
Usa tu analytics para identificar las páginas que obtienen más tráfico y dónde importa más TTFB. Para nosotros, eran páginas de categoría (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 analytics integrado de Vercel más logging 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 stale, se elevó 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 de migración completa |
|---|---|---|---|
| TTFB promedio (páginas de productos) | 180ms | 340ms | 95ms |
| TTFB promedio (páginas de categoría) | 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é | Desconocido (implícito) | 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 generara bajo demanda con almacenamiento en caché. La directiva cacheComponents significaba que esas páginas 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é explícito de componentes) más compilaciones más cortas significaba ahorros reales. Esa reducción de ~$320/mes se paga sola.
Trampas y problemas
Límites de serialización
La directiva "use cache" crea un límite de serialización. Todo lo pasado a un componente en caché como props debe ser serializable. Teníamos varios componentes que recibían funciones callback 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é perimetral de Vercel en la primera semana y tuvimos que ser más estratégicos sobre qué páginas obtuvieron valores expire largos. Las páginas de cola larga con 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á esa marca de tiempo en caché. Encontramos esto en una pantalla "ú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 de equipo: almacena en caché a nivel de página O a nivel de componente, no ambos, a menos que haya una razón clara.
Cuándo deberías y no deberías usar cacheComponents
Úsalo cuando:
- Tienes más de algunas cientos de páginas y los tiempos de compilación ISR son dolorosos
- Tu contenido tiene requisitos de actualización claros que varían por sección
- Necesitas control granular sobre qué se almacena en caché versus siempre-fresco
- Estás ejecutándose en Vercel o una plataforma que soporta las capas de caché de Next.js
- Quieres reducir costos de infraestructura en sitios con alto tráfico
No lo uses cuando:
- Tu sitio es lo suficientemente pequeño que SSG completo funciona bien
- Cada página es completamente dinámica (contenido específico del usuario en todas partes)
- No estás en una plataforma de alojamiento que soporta infraestructura de caché de Next.js
- Tu equipo es nuevo en Next.js -- familiarízate con lo básico primero
Si estás evaluando si tu proyecto necesita este nivel de control de caché, o si un framework diferente como Astro podría ser mejor para tu sitio centrado en contenido, eso vale la pena considerar antes de comprometerse con una migración.
Para proyectos donde el contenido proviene de múltiples fuentes de CMS headless, el sistema cacheTag en Next.js 16 funciona hermosamente con arquitecturas de CMS headless -- 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 almacenarse en caché y definir perfiles de caché personalizados usando cacheLife. Es la evolución estable de la directiva use cache que fue experimental en Next.js 15.
¿En qué se diferencia cacheComponents de ISR (Incremental Static Regeneration)?
ISR almacena en caché páginas completas y las revalida según un horario 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 de 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 deployments de Next.js auto-alojados pueden usar cacheComponents con el adaptador de caché del sistema de archivos, pero no obtendrás beneficios de distribución perimetral. Plataformas como Netlify y Cloudflare están agregando soporte, pero a partir de mediados de 2026, Vercel sigue siendo la implementación más completa.
¿Cómo invalido componentes en caché en Next.js 16?
Usas cacheTag() dentro de tu componente en caché para asignar etiquetas, luego llamas revalidateTag('tag-name') desde un server action, manejador de ruta o punto final de webhook. Esto invalida todos los componentes en caché con esa etiqueta. Es la misma API desde Next.js 15, pero es más útil ahora porque estás etiquetando componentes en caché explícitos en lugar de cachés de marco implícitos.
¿Reducirá cacheComponents mi factura de Vercel? Puede reducir significativamente los costos. En nuestro caso, las invocaciones de función cayeron un 54% porque las respuestas de componentes en caché se servían 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, por lo que todas las props deben ser serializables (strings, números, objetos simples, arrays). Las 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 e 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 basa en React's Server Components. Aunque la sintaxis de la directiva "use cache" eventualmente podría convertirse en un estándar de React, cacheLife y los sistemas cacheTag son APIs de Next.js. Si estás usando un framework como Remix o una configuración RSC personalizada, necesitarás estrategias de 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 tomó 7 semanas incluyendo testing 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 real son directos -- el tiempo se invierte en auditar tus necesidades de caché, probar flujos de invalidación y monitorear tasas de acierto de caché después del deployment. Si prefieres no ir solo, contáctanos -- hemos hecho esto un par de veces.