En las aplicaciones web modernas, los distintos tipos de páginas presentan requerimientos radicalmente dispares en cuanto a latencia de entrega, frescura de datos y carga sobre la infraestructura:
Next.js App Router integra estos escenarios en un modelo de renderizado híbrido y unificado. En lugar de la segregación obsoleta a nivel de página (getServerSideProps y getStaticProps del antiguo Pages Router), en la arquitectura moderna la estrategia se estipula de forma declarativa en los segmentos de ruta (Route Segments) y en cada llamada de acceso a datos (Data Cache).
En Next.js App Router conviven tres estrategias de renderizado esenciales:
cookies(), headers(), searchParams) o forzarse mediante export const dynamic = 'force-dynamic'.Cliente A Edge CDN Worker en background (Node.js)
│ │ │
(1) ├──── GET /product/42 ────────►│ │
│ │ [Caché fresca: age < 60s] │
│◄─── 200 OK (HTML/RSC) ───────┤ │
│ │ │
═══════╪══════════════════════════════╪══════════════════════════════════╪═══════
│ (Transcurren 65 segundos: la caché pasa a estado STALE) │
│ │ │
(2) │ Cliente B │ │
├──── GET /product/42 ────────►│ │
│ │ [¡Caché caducada! Retorna stale] │
│◄─── 200 OK (Copia STALE) ────┤ │
│ │ │
│ ├── Disparador de renderizado ────►│
│ │ ├── Consulta a BD
│ │ ├── Genera HTML/RSC
│ │◄── Nuevo HTML/RSC Payload ───────┤
│ │ [Caché renovada: age = 0s] │
│ │ │
(3) │ Cliente C │ │
├──── GET /product/42 ────────►│ │
│ │ [¡Caché fresca!] │
│◄─── 200 OK (Payload FRESCO) ─┤ │La página se genera durante el build (SSG) o por demanda del primer usuario. El HTML y el flujo RSC Flight resultantes se depositan en Full Route Cache y en la Edge CDN.
Durante el período estipulado (por ejemplo, revalidate = 60), todos los clientes reciben respuestas inmediatas de la CDN con TTFB inferior a 50 ms.
Cumplidos 60 segundos, la entrada se etiqueta como Stale. La siguiente solicitud recibe al instante la versión previa sin bloquearse.
Next.js activa asíncronamente un proceso en background para reevaluar la lógica de la página y consultar la base de datos.
Al compilar exitosamente, el nuevo HTML y el payload RSC sobrescriben la caché de forma atómica para todas las visitas posteriores.
¿Qué experimentará un usuario que solicita una página con revalidate = 60 segundos transcurridos 65 segundos desde su última generación?
El comportamiento de renderizado puede definirse explícitamente exportando constantes de configuración en page.tsx o layout.tsx:
// 1. Forzar SSR dinámico (sin almacenamiento en caché de página)
export const dynamic = 'force-dynamic';
// 2. Forzar SSG estático (las funciones dinámicas devuelven valores vacíos)
// export const dynamic = 'force-static';
// 3. ISR por tiempo: revalidar la página como máximo una vez cada 60 segundos
// export const revalidate = 60;
// 4. Comportamiento ante parámetros no previstos en generateStaticParams:
// true: generar bajo demanda en la primera visita y guardar en ISR (predeterminado)
// false: responder con 404 Not Found
export const dynamicParams = true;
export default async function DashboardPage() {
return <div>Panel de Control Dinámico</div>;
}fetch() en Next.js 15+#[!IMPORTANT] Estándar arquitectónico: Desde Next.js 15, las llamadas globales
fetch()NO se almacenan en caché por defecto (cache: 'no-store'). ¡Este comportamiento es exactamente opuesto al de Next.js 13 y 14!
Para controlar el almacenamiento en caché de peticiones de red, proporciona las opciones adecuadas en el objeto de configuración de fetch:
// 1. SSG: Almacenamiento en caché permanente (Permanent Data Cache)
const staticData = await fetch('https://api.example.com/config', {
cache: 'force-cache'
});
// 2. ISR por tiempo: Almacenamiento con revalidación periódica (1 hora)
const timedData = await fetch('https://api.example.com/products', {
next: {
revalidate: 3600,
tags: ['products-list'] // Etiqueta para purga bajo demanda
}
});
// 3. SSR: Petición sin caché (comportamiento predeterminado en Next.js 15+)
const liveData = await fetch('https://api.example.com/notifications', {
cache: 'no-store'
});revalidateTag y revalidatePath#La invalidación de caché guiada por eventos (por ejemplo, al guardar un producto en el panel de control o recibir un webhook desde un CMS) se ejecuta en el servidor mediante Server Actions o Route Handlers:
'use server';
import { revalidateTag, revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
interface UpdateProductDTO {
title: string;
price: number;
}
export async function updateProduct(id: string, data: UpdateProductDTO): Promise<{ success: boolean }> {
// 1. Modificación de registros en la base de datos
await db.product.update({
where: { id },
data
});
// 2. Invalidación puntual por etiqueta (afecta a los registros correspondientes en Data Cache)
revalidateTag(`product-${id}`);
// 3. Invalidación de la ruta del catálogo
revalidatePath('/catalog', 'page');
return { success: true };
}unstable_cache#Cuando se accede a datos mediante un ORM (Prisma, Drizzle, TypeORM), no existe una llamada fetch intermedia. Para incorporar estas operaciones al sistema de Data Cache e ISR, utiliza unstable_cache:
import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
// Envolver la consulta a base de datos en la capa Data Cache de Next.js
export const getCachedPost = unstable_cache(
async (slug: string) => {
return await db.post.findUnique({
where: { slug }
});
},
['post-by-slug'], // Claves de diferenciación de caché (Key parts)
{
revalidate: 300, // ISR: 5 minutos
tags: ['posts'] // Etiqueta para revalidación bajo demanda con revalidateTag('posts')
}
);En tiendas con 200.000 productos, compilar todas las páginas de antemano es ineficiente. Este patrón coordina generateStaticParams para los 100 productos más visitados con ISR automático bajo demanda para el resto:
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ id: string }>; // En Next.js 15+, el acceso es estrictamente asíncrono
}
// 1. Precompilar los 100 productos más populares en tiempo de build
export async function generateStaticParams() {
const popularProducts = await db.product.findMany({
where: { isPopular: true },
take: 100,
select: { id: true }
});
return popularProducts.map((p) => ({ id: p.id }));
}
// 2. Permitir la generación bajo demanda para los 199.900 artículos restantes al ser visitados
export const dynamicParams = true;
// 3. Revalidar la copia en background como máximo una vez cada 10 minutos
export const revalidate = 600;
export default async function ProductPage({ params }: PageProps) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) {
notFound();
}
return (
<article className="p-8 max-w-3xl mx-auto">
<h1 className="text-3xl font-bold">{product.title}</h1>
<p className="text-2xl font-mono text-accent-cyan mt-2">{product.price} €</p>
<div className="mt-4 text-text-muted leading-relaxed">{product.description}</div>
</article>
);
}import { NextRequest, NextResponse } from 'next/server';
import { revalidateTag } from 'next/cache';
export async function POST(request: NextRequest) {
const secret = request.nextUrl.searchParams.get('secret');
// Verificar el token de autorización
if (secret !== process.env.REVALIDATION_SECRET) {
return NextResponse.json({ message: 'Token no válido' }, { status: 401 });
}
const body = (await request.json()) as { tag?: string };
if (!body.tag) {
return NextResponse.json({ message: 'Etiqueta no suministrada' }, { status: 400 });
}
// Invalidación inmediata en Data Cache para las peticiones asociadas a esta etiqueta
revalidateTag(body.tag);
return NextResponse.json({ revalidated: true, tag: body.tag, now: Date.now() });
}¿Qué mecanismo actualiza con mayor precisión el bloque de productos recomendados sin forzar la recompilación de la página completa de catálogo?
En Next.js 13 y 14, las peticiones con fetch() se retenían indefinidamente en caché mediante force-cache. Gran parte de la comunidad asumía que cualquier consulta generaba páginas SSG automáticamente.
En Next.js 15+, la regla se invirtió por completo: fetch() se ejecuta con cache: 'no-store' por defecto. Escribir await fetch('https://api.com/items') sin declarar la política de caché convierte la ruta en Dynamic SSR, consultando el endpoint externo en cada visita.
Solución: Declara siempre explícitamente la estrategia deseada (force-cache para SSG o next: { revalidate: 60 } para ISR).
// Para SSG:
const res = await fetch(url, { cache: 'force-cache' });
// Para ISR:
const res = await fetch(url, { next: { revalidate: 60 } });Invocar funciones dinámicas como await cookies() o await headers() en cualquier componente de servidor (incluso en un widget secundario del footer) fuerza a Next.js a descartar la compilación estática y degradar la ruta completa a Dynamic SSR.
Solución:
<Suspense fallback={<CartSkeleton />}>: el armazón de la página se mantendrá en SSG/ISR, mientras el slot dinámico se entrega en streaming.// components/CartBadge.tsx (Server Component en la barra lateral)
import { cookies } from 'next/headers';
export async function CartBadge() {
const cookieStore = await cookies(); // ❌ Degrada toda la ruta a Dynamic SSR sin Suspense
const cartId = cookieStore.get('cartId')?.value;
return <span>{cartId ? 'Artículos: 1' : 'Cesta vacía'}</span>;
}Al agotarse el temporizador revalidate: 60, la primera visita recibida dispara la regeneración de la página en segundo plano. Si la compilación requiere 3 o 4 segundos y el sitio opera a 500 peticiones por segundo, la ausencia de bloqueos distribuidos puede inducir a decenas de workers concurrentes a bombardear la base de datos con consultas idénticas.
Solución arquitectónica: Next.js provee un mecanismo nativo de desduplicación de revalidación; no obstante, para consultas directas vía ORM, encapsula las llamadas con unstable_cache y gestiona la invalidación mediante webhooks bajo demanda (revalidateTag) en lugar de temporizadores de intervalo agresivo.
Ejecutar revalidatePath('/') solo invalida la página de inicio. Para refrescar el árbol global completo es imprescindible indicar el parámetro 'layout': revalidatePath('/', 'layout'). Sin embargo, abusar de este recurso en entornos productivos ocasiona picos de latencia en la base de datos al invalidar miles de rutas simultáneamente. Prioriza siempre revalidateTag().