Caching profundo en Next.js — Entiende y controla las cuatro capas de caching
Recipe
// 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.
Working Example
// ---- 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:
- Request memoization:
getProductByIdllamado desde múltiples componentes se ejecuta solo una vez por renderizado - Data cache: lista de productos cacheada por 1 hora, sub-5ms en solicitudes repetidas (vs 120ms sin caché)
- Full Route Cache: páginas de productos pre-renderizadas en tiempo de construcción vía
generateStaticParams - ISR: páginas cacheadas se revalidan cada hora automáticamente
- Invalidación dirigida:
revalidateTag("products")invalida todos los caches relacionados con productos después de mutaciones - Carga de base de datos reducida aproximadamente 95% para catálogos de productos con mucha lectura
Deep Dive
Cómo funciona
- Request Memoization es la deduplicación incorporada de React. Cuando la misma URL
fetch()o función envuelta encache()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. - Data Cache persiste las respuestas de
fetch()entre solicitudes y despliegues. Cuandonext: { 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). Usaunstable_cachepara fuentes de datos que no sean fetch como consultas a bases de datos. - Full Route Cache almacena el HTML pre-renderizado y la carga de RSC para rutas estáticas. Las páginas sin funciones dinámicas (
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. - Router Cache es un caché en memoria del lado del cliente que almacena la carga de RSC de rutas visitadas previamente. Cuando el usuario navega de vuelta a una página visitada, la versión cacheada se muestra instantáneamente. Las páginas dinámicas se cachean por 30 segundos; las páginas estáticas se cachean por 5 minutos.
revalidateTaginvalida todas las entradas cacheadas (Data Cache y Full Route Cache) asociadas con una etiqueta específica. Esto es más dirigido querevalidatePath, que invalida todo en una ruta.revalidatePathinvalida el Full Route Cache para una ruta específica y dispara un re-renderizado en la siguiente solicitud.
Variations
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 backgroundNotas de TypeScript
unstable_cacheacepta un genérico:unstable_cache<Product[]>(fn, keys, opts).revalidateTagyrevalidatePathestán tipados para aceptar parámetrosstring.- El tipo de retorno de
generateStaticParamsse infiere de los parámetros del segmento de ruta. cache()de React preserva la firma del tipo de la función envuelta.
Gotchas
-
cookies()oheaders()deshabilitando el caching — Llamar acookies()en cualquier lugar de una ruta hace que la ruta completa sea dinámica, deshabilitando Full Route Cache. Fix: Mueve las llamadas acookies()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
revalidateTagorevalidatePathdeja 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. -
revalidatePathes 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: UsarevalidatePath("/products", "layout")para invalidar el layout y todas las rutas secundarias, o usarevalidateTagpara 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 enunstable_cachepara 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.
Alternatives
| 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 |
FAQs
¿Cuáles son las cuatro capas de caching en Next.js y qué cachea cada una?
- Request Memoization: Deduplica llamadas idénticas dentro de un único renderizado de servidor.
- Data Cache: Persiste las respuestas de
fetch()entre solicitudes y despliegues. - Full Route Cache: Almacena el HTML pre-renderizado y la carga de RSC para rutas estáticas.
- Router Cache: Caché en memoria del lado del cliente de rutas visitadas previamente.
¿Cuál es la diferencia entre revalidateTag y revalidatePath?
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.- Usa etiquetas para invalidación dirigida; usa ruta para invalidación amplia a nivel de ruta.
¿Cómo funciona la función cache() de React para request memoization?
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.¿Cuándo deberías usar unstable_cache en lugar de fetch con next.revalidate?
- El caching de
fetch()solo funciona con la API de Fetch. - Los clientes de base de datos (Prisma, Drizzle) no funcionan con el Data Cache en absoluto.
- Fix: Envuelve consultas a la base de datos en
unstable_cachepara caching persistente entre solicitudes.
Gotcha: ¿Por qué llamar a cookies() hace que una ruta completa sea dinámica?
cookies()es una función dinámica que requiere datos por solicitud.- Llamarla en cualquier lugar de una ruta deshabilita Full Route Cache para esa ruta completa.
- Fix: Mueve las llamadas a
cookies()al Componente de Servidor específico que las necesita, o usa middleware.
¿Cuál es el patrón stale-while-revalidate en el Data Cache?
- Después de
revalidate: Nsegundos, la respuesta cacheada se sirve (antigua) mientras se ejecuta una revalidación en segundo plano. - La siguiente solicitud después de la revalidación recibe los datos frescos.
- Los usuarios nunca esperan la revalidación -- obtienen datos antiguos instantáneamente seguidos de datos frescos en la siguiente visita.
¿Cómo depuras qué capa de caching está sirviendo una respuesta?
- Habilita
logging: { fetches: { fullUrl: true } }ennext.config.tspara registrar URLs de fetch con estado de caché. - Verifica los encabezados de respuesta
x-nextjs-cache:HIT,MISS, oSTALE. HIT= Full Route Cache,MISS= renderizado bajo demanda,STALE= servido antiguo mientras se revalida.
Gotcha: ¿Por qué un usuario podría seguir viendo datos obsoletos después de que llames a revalidateTag?
- El Router Cache del lado del cliente podría seguir teniendo una versión antigua por hasta 30 segundos (dinámico) o 5 minutos (estático).
- Fix: Usa
router.refresh()para forzar una búsqueda fresca desde el servidor, o acepta la ventana de antigüedad.
¿Cómo tipas unstable_cache con un tipo de retorno genérico en TypeScript?
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[]>¿Cómo preserva cache() de React la firma de tipo TypeScript de la función envuelta?
cache()devuelve una función con la misma firma de tipo que la original.- Los parámetros y el tipo de retorno se preservan completamente -- no es necesario explícitamente genéricos.
- El autocompletado del IDE y la verificación de tipos funcionan idénticamente en la versión cacheada.
¿Cuál es la diferencia entre export const dynamic = "force-dynamic" y cache: "no-store"?
dynamic = "force-dynamic"excluye la ruta completa de todas las capas de caching.cache: "no-store"en unfetch()específico solo excluye ese fetch del Data Cache.- Prefiere control por fetch para granularidad; usa nivel de ruta solo cuando cada fetch debe ser dinámico.
¿Cómo interactúa generateStaticParams con el Full Route Cache?
generateStaticParamspre-renderiza páginas de rutas dinámicas específicas en tiempo de construcción.- Estas páginas se almacenan en el Full Route Cache y se sirven instantáneamente.
- Combinado con
revalidate, usan ISR para regenerarse periódicamente sin una reconstrucción completa.
¿Qué sucede si dos llamadas a unstable_cache usan la misma clave de caché pero consultas diferentes?
- Se sobrescriben mutuamente -- el resultado de la segunda llamada reemplaza el primero.
- Fix: Usa claves de caché descriptivas y únicas que incluyan parámetros de consulta:
["products", category, sortBy].
Related
- Data Fetching Performance — Parallel fetching and waterfall elimination
- Server Component Performance — Server-side data fetching patterns
- Suspense & Streaming — Streaming cached and uncached sections
- Performance Checklist — Caching audit items