Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Layer 1: Request Memoization - automatic dedup within a single render
// Both components call the same function, but only ONE database query executes
async function getUser(id: string) {
// React deduplicates this automatically during a single render pass
return db.user.findUnique({ where: { id } });
}
// Layer 2: Data Cache - persistent cache for fetch() results
const data = await fetch("https://api.example.com/products", {
next: { revalidate: 3600, tags: ["products"] },
});
// Layer 3: Full Route Cache - pre-rendered HTML for static routes
// Automatic for pages without dynamic functions (cookies, headers, searchParams)
// Layer 4: Router Cache - client-side cache for visited routes
// Automatic for all navigations, cached for 30s (dynamic) or 5min (static)
// Invalidation
import { revalidateTag, revalidatePath } from "next/cache";
// Targeted: invalidate all fetches tagged "products"
revalidateTag("products");
// Broad: invalidate a specific route
revalidatePath("/products");Cuándo usarlo: Cuando necesites controlar la frescura de datos versus rendimiento. Entender estas capas previene bugs de datos obsoletos y permite caching agresivo para páginas frecuentemente accedidas.
// ---- BEFORE: No caching strategy - every page load hits the database ----
// app/products/page.tsx
export const dynamic = "force-dynamic"; // Opts out of ALL caching
export default async function ProductsPage() {
// Hits database on EVERY request - 120ms per visit
const products = await db.product.findMany({
include: { category: true },
orderBy: { createdAt: "desc" },
});
// Same query executed AGAIN for the count
const allProducts = await db.product.findMany();
const totalCount = allProducts.length;
return (
<div>
<h1>Products ({totalCount})</h1>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
// app/products/[id]/page.tsx
export const dynamic = "force-dynamic";
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
// Hits database on every visit - even for the same product
const product = await db.product.findUnique({
where: { id },
include: { reviews: true, seller: true },
});
return <ProductDetail product={product} />;
}
// Result: 120ms per page load, no caching, database under constant load
// ---- AFTER: Layered caching strategy - sub-50ms for cached pages ----
// lib/data/products.ts - Centralized data access with caching
import { cache } from "react";
import { unstable_cache } from "next/cache";
// Layer 1: Request Memoization - dedup within a single render
// React's cache() ensures this only runs once per render pass,
// even if called from multiple Server Components
export const getProductById = cache(async (id: string) => {
return db.product.findUnique({
where: { id },
include: { reviews: true, seller: true },
});
});
// Layer 2: Data Cache - persistent cache across requests
export const getProducts = unstable_cache(
async () => {
return db.product.findMany({
include: { category: true },
orderBy: { createdAt: "desc" },
});
},
["products-list"], // Cache key
{
revalidate: 3600, // Revalidate every hour
tags: ["products"], // Tag for targeted invalidation
}
);
export const getProductCount = unstable_cache(
async () => {
return db.product.count();
},
["product-count"],
{
revalidate: 3600,
tags: ["products"],
}
);
// app/products/page.tsx - Uses cached data
export default async function ProductsPage() {
// Both use the "products" cache - fast after first request
const [products, totalCount] = await Promise.all([
getProducts(),
getProductCount(),
]);
return (
<div>
<h1>Products ({totalCount})</h1>
{products.map((p) => (
<ProductCard key={p.id} product={p} />
))}
</div>
);
}
// Layer 3: Full Route Cache - pre-render product pages at build time
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { id: true } });
return products.map((p) => ({ id: p.id }));
}
// app/products/[id]/page.tsx - Statically generated + ISR
export const revalidate = 3600; // ISR: regenerate every hour
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await getProductById(id);
if (!product) notFound();
return <ProductDetail product={product} />;
}
// Invalidation: Server Action after product update
// app/actions/products.ts
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(id: string, data: ProductUpdateData) {
await db.product.update({ where: { id }, data });
// Invalidate the data cache for all product fetches
revalidateTag("products");
// Invalidate the specific product page route cache
revalidatePath(`/products/${id}`);
}
export async function deleteProduct(id: string) {
await db.product.delete({ where: { id } });
// Invalidate everything tagged "products"
revalidateTag("products");
// Invalidate the product listing page
revalidatePath("/products");
}Lo que esto demuestra:
getProductById llamado desde múltiples componentes se ejecuta solo una vez por renderizadogenerateStaticParamsrevalidateTag("products") invalida todos los caches relacionados con productos después de mutacionesfetch() o función envuelta en cache() se llama múltiples veces durante un único renderizado de servidor, solo una ejecución ocurre. El resultado se comparte entre todos los sitios de llamada. Esto es automático y no requiere configuración.fetch() entre solicitudes y despliegues. Cuando next: { revalidate: N } se establece, la respuesta cacheada se sirve por N segundos. Después de N segundos, la siguiente solicitud dispara una revalidación en segundo plano (patrón stale-while-revalidate). Usa unstable_cache para fuentes de datos que no sean fetch como consultas a bases de datos.cookies(), headers(), searchParams) se renderizan estáticamente en tiempo de construcción. Las páginas dinámicas se renderizan en la primera solicitud y se cachean.revalidateTag invalida todas las entradas cacheadas (Data Cache y Full Route Cache) asociadas con una etiqueta específica. Esto es más dirigido que revalidatePath, que invalida todo en una ruta.revalidatePath invalida el Full Route Cache para una ruta específica y dispara un re-renderizado en la siguiente solicitud.Opting out of caching per fetch:
// No caching - always fresh data
const data = await fetch("https://api.example.com/live-prices", {
cache: "no-store",
});
// Equivalent: dynamic route segment config
export const dynamic = "force-dynamic";
export const revalidate = 0;Time-based revalidation (ISR):
// Page-level revalidation
export const revalidate = 60; // Revalidate every 60 seconds
// Fetch-level revalidation
const data = await fetch(url, {
next: { revalidate: 300 }, // This specific fetch caches for 5 minutes
});On-demand revalidation in Server Actions:
"use server";
import { revalidateTag, revalidatePath } from "next/cache";
export async function publishPost(id: string) {
await db.post.update({
where: { id },
data: { published: true },
});
// Granular: only invalidate blog-related caches
revalidateTag("blog-posts");
revalidateTag(`post-${id}`);
// Broad: invalidate the entire blog section
revalidatePath("/blog", "layout");
}Cache debugging with headers:
// next.config.ts - expose cache status headers
const nextConfig = {
logging: {
fetches: {
fullUrl: true, // Log full fetch URLs with cache status
},
},
};
// Check response headers in DevTools:
// x-nextjs-cache: HIT - served from Full Route Cache
// x-nextjs-cache: MISS - rendered on demand, now cached
// x-nextjs-cache: STALE - served stale, revalidating in backgroundunstable_cache acepta un genérico: unstable_cache<Product[]>(fn, keys, opts).revalidateTag y revalidatePath están tipados para aceptar parámetros string.generateStaticParams se infiere de los parámetros del segmento de ruta.cache() de React preserva la firma del tipo de la función envuelta.cookies() o headers() deshabilitando el caching - Llamar a cookies() en cualquier lugar de una ruta hace que la ruta completa sea dinámica, deshabilitando Full Route Cache. Fix: Mueve las llamadas a cookies() al Componente de Servidor específico que las necesita, o usa middleware para verificaciones de autenticación.
Datos obsoletos después de mutaciones - Actualizar datos sin llamar a revalidateTag o revalidatePath deja páginas cacheadas mostrando datos antiguos. Fix: Siempre revalida después de Server Actions que mutan datos.
Colisiones de claves en unstable_cache - Dos consultas diferentes con la misma clave de caché se sobrescriben mutuamente. Fix: Usa claves de caché descriptivas y únicas que incluyan los parámetros de consulta: ["products", category, sortBy].
Router Cache mostrando páginas antiguas - El Router Cache del lado del cliente puede mostrar una versión antigua de una página incluso después de la revalidación del servidor. Fix: Usa router.refresh() para forzar una búsqueda fresca desde el servidor, o acepta la ventana de 30 segundos de antigüedad.
revalidatePath es más amplio de lo esperado - revalidatePath("/products") invalida la página de listado de productos pero no las páginas de productos individuales. Fix: Usa revalidatePath("/products", "layout") para invalidar el layout y todas las rutas secundarias, o usa revalidateTag para control más fino.
Caching a nivel de fetch en Server Components con clientes de base de datos - El caching de fetch() solo funciona con la API de Fetch. Prisma, Drizzle y otros clientes de base de datos no funcionan con el Data Cache. Fix: Envuelve consultas a la base de datos en unstable_cache para caching persistente.
El modo desarrollo no cachea - En next dev, el caching está deshabilitado por defecto para simplificar el desarrollo. Fix: Prueba el comportamiento del caching en compilaciones de producción: npm run build && npm start.
| Approach | Trade-off |
|---|---|
| Next.js built-in caching | Integrado; modelo mental complejo con 4 capas |
| Redis o Upstash | Caché externo; más control, más infraestructura |
| CDN caching (Cloudflare, Vercel Edge) | A nivel de edge; la invalidación de caché es más difícil |
| SWR stale-while-revalidate | Del lado del cliente; sin caché de servidor, agrega JS del cliente |
| ISR (Incremental Static Regeneration) | Basado en tiempo; datos obsoletos dentro de la ventana de revalidación |
| On-demand revalidation | Preciso; requiere llamadas explícitas después de cada mutación |
| Static generation (SSG) | Solo en tiempo de construcción; sin datos en tiempo de ejecución, lo más rápido posible |
fetch() entre solicitudes y despliegues.revalidateTag("products") invalida todas las entradas cacheadas (Data Cache + Full Route Cache) con esa etiqueta -- más dirigida.revalidatePath("/products") invalida el Full Route Cache para una ruta específica.import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
// Called in Component A and Component B during the same render:
// Only ONE database query executes; both receive the same result.fetch() solo funciona con la API de Fetch.unstable_cache para caching persistente entre solicitudes.cookies() es una función dinámica que requiere datos por solicitud.cookies() al Componente de Servidor específico que las necesita, o usa middleware.revalidate: N segundos, la respuesta cacheada se sirve (antigua) mientras se ejecuta una revalidación en segundo plano.logging: { fetches: { fullUrl: true } } en next.config.ts para registrar URLs de fetch con estado de caché.x-nextjs-cache: HIT, MISS, o STALE.HIT = Full Route Cache, MISS = renderizado bajo demanda, STALE = servido antiguo mientras se revalida.router.refresh() para forzar una búsqueda fresca desde el servidor, o acepta la ventana de antigüedad.import { unstable_cache } from "next/cache";
const getProducts = unstable_cache<Product[]>(
async () => {
return db.product.findMany();
},
["products-list"],
{ revalidate: 3600, tags: ["products"] }
);
// Return type is inferred as Promise<Product[]>cache() devuelve una función con la misma firma de tipo que la original.dynamic = "force-dynamic" excluye la ruta completa de todas las capas de caching.cache: "no-store" en un fetch() específico solo excluye ese fetch del Data Cache.generateStaticParams pre-renderiza páginas de rutas dinámicas específicas en tiempo de construcción.revalidate, usan ISR para regenerarse periódicamente sin una reconstrucción completa.["products", category, sortBy].Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥