Next.js
Partial Prerendering y Cache Components
Explica cómo Cache Components combina una shell prerenderizada, contenido cacheado y regiones dinámicas bajo Suspense dentro de una misma ruta.
- Última actualización
- Actualizada
- Nivel
- Profundización
Next.js
Explica cómo Cache Components combina una shell prerenderizada, contenido cacheado y regiones dinámicas bajo Suspense dentro de una misma ruta.
Partial Prerendering no es un modo separado que eliges para toda la página. Con Cache Components, Next.js intenta construir una shell estática, incorpora resultados cacheables y deja huecos bajo Suspense para trabajo que necesita la request. El resultado combina respuesta inmediata y contenido dinámico dentro del mismo árbol.
Habilitación:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;Modelo:
route tree at prerender
↓
static synchronous content
+ use cache results
+ Suspense fallbacks
↓
prerendered shell
↓ request
uncached/request-dependent regions execute
↓
stream completed segmentsPartial Prerendering es el resultado de combinar esas reglas; no necesitas un flag ppr separado en el modelo actual.
La clasificación monolítica obliga a elegir:
static route
→ rápida, pero sin datos por request
dynamic route
→ personalizada, pero todo espera servidorMuchas pantallas tienen partes diferentes:
product page
├─ navigation compartida
├─ product description cacheable
├─ stock fresco
├─ cart por usuario
└─ add-to-cart clienteConvertir toda la route en dinámica desperdicia contenido reutilizable. Convertirla toda en estática serviría stock y carrito incorrectos.
Durante prerendering, Next.js intenta ejecutar el árbol.
function ProductHeading() {
return <h1>Catálogo de productos</h1>;
}Entra a la shell automáticamente.
async function ProductDetails({ slug }: { slug: string }) {
"use cache";
cacheLife("hours");
cacheTag(`product:${slug}`);
const product = await getProduct(slug);
return <ProductContent product={product} />;
}Next.js puede llenar/reutilizar la entrada y añadir su output a la shell.
async function PersonalCart() {
const store = await cookies();
const cart = await getCart(store.get("cart")?.value);
return <CartSummary cart={cart} />;
}Necesita request actual. Debe quedar bajo Suspense:
<Suspense fallback={<CartSkeleton />}>
<PersonalCart />
</Suspense>El skeleton entra a la shell; el carrito se transmite después.
export default async function ProductPage({
params,
}: PageProps<"/products/[slug]">) {
const { slug } = await params;
return (
<main>
<SiteNavigation />
<ProductDetails slug={slug} />
<Suspense fallback={<StockSkeleton />}>
<LiveStock slug={slug} />
</Suspense>
<Suspense fallback={<CartSkeleton />}>
<PersonalCart />
</Suspense>
<AddToCartButton slug={slug} />
</main>
);
}Clasificación:
SiteNavigation → static or cached shell
ProductDetails → cached shell
LiveStock → request-time stream
PersonalCart → request-time personalized stream
AddToCartButton → hydrated Client ComponentCon Cache Components, una operación externa no cacheada fuera de Suspense es ambigua:
export default async function Page() {
const products = await db.product.findMany();
return <ProductList products={products} />;
}Next.js no debe ejecutar arbitrariamente esa query durante build ni sabe si aceptas reutilización. El error obliga a elegir:
async function getProducts() {
"use cache";
cacheLife("minutes");
return db.product.findMany();
}function Page() {
return (
<Suspense fallback={<ProductsSkeleton />}>
<Products />
</Suspense>
);
}No añadas Suspense solo para silenciar el build. Elige según freshness y audiencia.
cookies().headers().searchParams.connection().Estas APIs indican que el valor no existe durante prerendering.
No se leen directamente dentro de use cache normal.
Patrón:
async function LocalizedCatalog() {
const store = await cookies();
const currency = normalizeCurrency(store.get("currency")?.value);
return <CachedCatalog currency={currency} />;
}
async function CachedCatalog({ currency }: { currency: Currency }) {
"use cache";
cacheLife("hours");
const products = await getCatalog(currency);
return <ProductList products={products} />;
}El valor seguro y compartible forma parte de la key.
import { connection } from "next/server";
async function RequestNonce() {
await connection();
return <span>{crypto.randomUUID()}</span>;
}Indica que el código posterior debe esperar request aunque no utilice cookies/headers.
No lo uses para hacer dinámica una route completa por costumbre. Colócalo en la región mínima.
Puede aplicarse a:
La key incluye identidad de función, argumentos serializables y closures capturadas.
Cuanto más alto el scope, más partes comparten lifetime e invalidación.
Cachea cerca de los datos cuando las regiones tienen políticas diferentes. Cachea una page completa solo si todo el output comparte semántica.
cacheLife({
stale: 300,
revalidate: 3600,
expire: 86400,
});stale: reutilización cliente antes de volver al servidor.revalidate: cuándo regenerar en background.expire: cuándo exigir una versión nueva.Un contenido muy corto puede no incluirse en prerendering. Si realmente cambia cada pocos segundos, probablemente pertenece a una región dinámica.
cacheTag(`product:${slug}`, "products");La Action/CMS invalida:
updateTag(`product:${slug}`);
revalidateTag("products", "max");Tags permiten que shell y otras routes dependientes se actualicen sin reconstruir todo.
async function CachedChrome({ children }: { children: React.ReactNode }) {
"use cache";
const navigation = await getNavigation();
return (
<AppShell navigation={navigation}>
{children}
</AppShell>
);
}children puede ser pass-through si el scope cacheado no lo inspecciona. El padre compone contenido dinámico fuera de la caché.
Esto permite:
cached shell
└─ dynamic child slotgenerateMetadata también puede necesitar datos cacheados o request context. Si metadata usa la misma lectura de producto, reutiliza un loader cacheado.
Bots que requieren head completo pueden bloquear hasta metadata; no hagas consultas innecesariamente lentas.
<Suspense fallback={<PageSkeleton />}>
<ProductHeader />
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews />
</Suspense>
</Suspense>La boundary exterior puede retrasar demasiado contenido si ProductHeader suspende. Diseña shell fuera de la boundary cuando sea estable.
Demasiadas boundaries generan parpadeos y una experiencia fragmentada.
Una navegación hacia una route con shell prefetcheada puede mostrar fallback instantáneamente.
Durante revalidaciones dentro de la misma UI, una transition o contenido stale puede ser mejor que reemplazarlo por skeleton.
PPR no decide automáticamente la UX de cada actualización.
Un not found crítico puede producir status correcto.
Headers ya están enviados; la UI puede mostrar not-found/error dentro del stream con status 200 y noindex según Next.js.
Si el status es requisito, resuelve existencia antes de la primera boundary que transmite.
Una query cacheada puede conservar una entidad eliminada hasta invalidación; la mutation debe actualizar tags.
Nunca incluyas en shell cacheada:
La shell puede contener layout y datos públicos. Las regiones privadas esperan request.
Una caché por userId puede ser técnicamente posible, pero aumenta cardinalidad y riesgo. Dynamic rendering suele ser más seguro para datos personales.
Un Client Component puede vivir dentro de shell prerenderizada:
server generates initial HTML
→ browser downloads client chunk
→ hydratePPR no elimina hydration. Mantén boundaries cliente pequeñas para que la shell visible se vuelva interactiva pronto.
La caché puede vivir:
Self-hosting con múltiples réplicas necesita coordinación de invalidación/cache si quieres consistencia.
Un deploy cambia build ID y funciones cacheadas, produciendo nuevas keys/output. Conserva assets/chunks antiguos durante una ventana para tabs abiertas.
PPR requiere runtime para completar regiones dinámicas. output: "export" no puede transmitir cookies() o consultas request-time después del build.
Suspense por sí solo no crea un servidor.
Notebook navigation → cache days, tag notebook
Note content → cache hours, tag note:id
Search query → request/searchParams dynamic
Current user edit tools → dynamic/authenticated
Theme toggle → Client ComponentPage pública entrega shell y nota cacheada. El buscador puede transmitir resultados por query. Las herramientas de edición no se incluyen para visitantes.
Al editar una nota, invalida note:id, notebook index y sitemap si el contenido se publica/despublica.
Sin Cache Components era común:
export const dynamic = "force-dynamic";
export const revalidate = 3600;Con Cache Components:
force-dynamic; coloca trabajo request-time bajo Suspense.force-static; deja que Next.js extraiga shell.revalidate a cacheLife en el scope correcto.fetchCache y runtimes incompatibles.Migra por rutas con tests, no mediante reemplazo global.
PPR implica shell prerenderizada con mezcla de contenido.
Hace que el documento espere request y reduce shell compartida.
Mezcla lifetimes diferentes.
La shell se reduce a un spinner.
Fuga entre usuarios/tenants.
Contenido obsoleto o regeneración excesiva.
No existe runtime.
Modelo contradictorio y errores de build.
use cache declara reutilización.connection() y Suspense?use cache y fallbacks de Suspense.Internacionalización en App Router aplica routing, headers, cookies, metadata y caché a múltiples idiomas y regiones.