Next.js
Static y dynamic rendering
Explica cuándo una región puede prerenderizarse y cuándo necesita request time, junto con Cache Components, Suspense, use cache y datos privados.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cuándo una región puede prerenderizarse y cuándo necesita request time, junto con Cache Components, Suspense, use cache y datos privados.
Static y dynamic no describen tipos de componentes. Describen si una parte de la respuesta puede prepararse sin una petición concreta o si necesita ejecutarse con datos del request o una fuente que debe consultarse en ese momento. En Next.js 16, Cache Components permite combinar ambas dentro de la misma ruta.
Una región es prerenderizable cuando Next.js puede calcular un resultado reutilizable antes de conocer al usuario o request actual.
Una región es dinámica cuando depende de:
sin request context + resultado reutilizable
→ static/cached shell
request context o fresh runtime data
→ dynamic regionEl objetivo no es convertir toda la aplicación en estática. Es maximizar trabajo reutilizable sin servir información incorrecta u obsoleta.
Next.js clasifica rutas con opciones de fetch, Dynamic APIs y route segment configs:
export const dynamic = "force-dynamic";
export const revalidate = 3600;Se habilita explícitamente:
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;Entonces expresas decisiones localmente:
static synchronous UI
→ entra a shell
async reusable data
→ use cache
request-time data
→ SuspenseOpciones como dynamic = "force-dynamic", dynamic = "force-static" y algunos configs antiguos ya no son necesarias o no están soportadas bajo Cache Components. No mezcles ambos modelos en la misma explicación.
Durante build, Next.js recorre el árbol:
component tree
↓
¿puede completarse sin request ni recurso externo?
├─ sí → static shell
└─ no
├─ use cache → llenar/reutilizar caché e incluir resultado
└─ Suspense → incluir fallback y ejecutar en requestSi una operación no cacheada queda fuera de Suspense, desarrollo y build producen un error para obligarte a declarar intención.
function MarketingHeader() {
return (
<header>
<h1>Controla tu negocio desde un solo lugar</h1>
<p>Inventario, pedidos y reportes.</p>
</header>
);
}No accede a datos externos ni request context. Puede formar parte de la shell sin directiva.
No escribas use cache para JSX puramente síncrono; ya es prerenderizable.
async function LatestOrders() {
const orders = await orderRepository.listLatest();
return <OrdersTable orders={orders} />;
}Una query externa no se ejecuta automáticamente durante prerendering con Cache Components porque puede ser lenta, fallar o necesitar datos frescos.
Debes decidir.
<Suspense fallback={<OrdersSkeleton />}>
<LatestOrders />
</Suspense>El fallback entra a la shell; la query se ejecuta durante request y se transmite.
async function LatestOrders() {
"use cache";
cacheLife("minutes");
cacheTag("orders");
const orders = await orderRepository.listLatest();
return <OrdersTable orders={orders} />;
}El resultado puede incluirse en shell o reutilizarse en runtime.
APIs vinculadas a la petición actual incluyen:
cookies().headers().searchParams de la page.connection() para diferir explícitamente al request.No pueden leerse directamente dentro de un scope use cache normal.
Ejemplo:
import { cookies } from "next/headers";
async function CurrentUserMenu() {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session")?.value;
const user = await getUserBySession(sessionId);
return <UserMenu user={user} />;
}Debe existir bajo Suspense cuando Cache Components está activo.
Lee el request fuera del scope y pasa un valor seguro:
async function UserCurrency() {
const cookieStore = await cookies();
const currency = parseCurrency(cookieStore.get("currency")?.value);
return <CachedPriceList currency={currency} />;
}
async function CachedPriceList({ currency }: { currency: Currency }) {
"use cache";
cacheLife("hours");
const prices = await getPrices(currency);
return <PriceList prices={prices} />;
}La key incluye currency. Todos los usuarios con la misma moneda pueden compartir el resultado.
No pases session ID si eso crearía una entrada privada reutilizable sin una política consciente.
Existe para casos donde un scope cacheado necesita request data privada bajo contratos específicos. Es una herramienta especializada para compliance o refactors difíciles.
No la utilices como opción predeterminada para personalización. Una caché privada puede aumentar cardinalidad, memoria y complejidad.
Cuando necesitas ejecutar por request pero no lees cookies, headers o search params:
import { connection } from "next/server";
async function RequestTimestamp() {
await connection();
const timestamp = Date.now();
return <time>{timestamp}</time>;
}La llamada señala que el código posterior depende del request y no debe prerenderizarse.
Sin esta señal, operaciones no deterministas podrían ejecutarse durante build y quedar fijas.
Math.random();
Date.now();
crypto.randomUUID();No implican automáticamente datos por request bajo todos los modelos. Si necesitas un valor nuevo por solicitud, ejecútalo después de una Dynamic API o connection().
Si solo necesitas un ID estable de accesibilidad, usa useId, no random.
Una route puede quedar completamente prerenderizada cuando sus layouts/pages y dependencias completan el build:
export default function AboutPage() {
return (
<main>
<h1>Acerca de</h1>
<CompanyStory />
</main>
);
}Puede contener Client Components. Su HTML inicial y RSC payload se preparan, mientras el JavaScript cliente se entrega para hidratación.
async function BlogPosts() {
"use cache";
cacheLife("days");
cacheTag("posts");
const posts = await cms.listPublishedPosts();
return <PostsGrid posts={posts} />;
}Un webhook del CMS puede llamar una Action/endpoint que ejecute:
revalidateTag("posts", "max");El contenido puede servirse stale mientras se actualiza en background según semántica stale-while-revalidate.
Para read-your-own-writes dentro de una Action, updateTag expira inmediatamente la entrada.
async function DashboardContent() {
const session = await requireSession();
const summary = await getDashboardForUser(session.user.id);
return <Dashboard summary={summary} />;
}export default function DashboardPage() {
return (
<main>
<DashboardShell />
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent />
</Suspense>
</main>
);
}La shell puede prerenderizarse; la región privada espera request.
Para rutas dinámicas:
export async function generateStaticParams() {
const products = await listPopularProducts();
return products.map(({ slug }) => ({ slug }));
}Esto prepara combinaciones conocidas. Bajo Cache Components, el resto del árbol todavía debe cumplir reglas de shell/cache/dynamic.
No genera permisos ni garantiza que los registros sigan existiendo.
Controla si rutas no generadas son admitidas. Puede ser útil para documentación con conjunto cerrado.
En catálogos que cambian, deshabilitar parámetros dinámicos puede producir 404 hasta el siguiente build.
Una page que utiliza searchParams depende de la URL del request:
export default function ProductsPage({
searchParams,
}: PageProps<"/products">) {
return (
<Suspense fallback={<ProductsSkeleton />}>
<Products searchParams={searchParams} />
</Suspense>
);
}No pases la Promise de searchParams a un use cache y la esperes allí: puede bloquear el build porque el valor no existe hasta request.
Espera fuera, valida y pasa valores serializables a una función cacheada si el resultado puede compartirse.
Ejemplo problemático:
export default async function Page() {
const products = await productRepository.list();
return <ProductList products={products} />;
}Con Cache Components, Next.js no sabe si debe ejecutar la query en build, cachearla o esperar request.
Correcciones:
async function getProducts() {
"use cache";
return productRepository.list();
}export default function Page() {
return (
<Suspense fallback={<ProductSkeleton />}>
<Products />
</Suspense>
);
}La elección depende de freshness, no de silenciar el error.
Sin Cache Components puedes encontrar:
export const dynamic = "force-static";
export const revalidate = 3600;
export const fetchCache = "force-cache";Estas opciones pertenecen al modelo previo y pueden seguir siendo relevantes en proyectos que no habilitan Cache Components.
Con Cache Components:
force-dynamic; el contenido no cacheado puede ejecutarse dinámicamente bajo Suspense.force-static; deja que el prerender detecte shell y usa use cache donde corresponde.revalidate de segmento por cacheLife en scopes cacheados.Sigue el upgrade guide y no hagas una migración mecánica sin tests.
En el modelo previo, Next.js extiende fetch:
fetch(url, {
cache: "force-cache",
next: { revalidate: 3600, tags: ["products"] },
});Con Cache Components, puedes envolver la función o componente con use cache y describir lifetime/tags de manera uniforme para fetch, ORM o SDK.
Esto evita que una consulta ORM tenga un modelo distinto por no usar HTTP.
use cache utiliza almacenamiento que depende del entorno:
No asumas que una caché en memoria es global entre regiones o réplicas.
Un output estático exige que todas las rutas puedan resolverse sin runtime servidor. Una región dinámica no puede funcionar simplemente bajo Suspense después del deploy si no existe servidor que la ejecute.
Alternativas:
Una key de caché incluye argumentos serializables y valores capturados. Riesgos:
getProject(projectId)Podría reutilizar resultado sin diferenciar organización si el ID no es global o la autorización está fuera.
Puede filtrar información.
Una entrada cacheada puede sobrevivir a una revocación. No caches decisiones sensibles sin lifetime e invalidación apropiados.
Cachea el resultado autorizado o datos públicos; evita cachear una entidad sensible antes de aplicar scope y luego reutilizarla para otro usuario.
export default function ProductPage({ params }: PageProps<"/products/[slug]">) {
return (
<main>
<Suspense fallback={<ProductSkeleton />}>
<CachedProduct params={params} />
</Suspense>
<Suspense fallback={<StockSkeleton />}>
<LiveStock params={params} />
</Suspense>
<Suspense fallback={<CartSkeleton />}>
<PersonalCart />
</Suspense>
</main>
);
}Diseño mejorado evitando Promises runtime dentro de cache:
async function ProductResolver({ params }: PageProps<"/products/[slug]">) {
const { slug } = await params;
return (
<>
<CachedProduct slug={slug} />
<LiveStock slug={slug} />
</>
);
}
async function CachedProduct({ slug }: { slug: string }) {
"use cache";
cacheLife("hours");
cacheTag(`product:${slug}`);
const product = await getProductBySlug(slug);
if (!product) notFound();
return <ProductDetails product={toProductView(product)} />;
}Pregunta por cada región:
Ejecuta next build y revisa rutas/shells.
Puedes activar logging de caché compatible para observar hits, fills e invalidación.
Prueba primer request, segundo request, expiración, mutación y usuarios distintos.
Prueba bajo la plataforma real; memoria local no representa serverless distribuido.
Usa dos sesiones/tenants y confirma aislamiento.
Pierde reutilización y aumenta coste.
Sirve datos obsoletos o privados.
El scope normal no admite request APIs directas. Extrae valores.
Puede provocar timeout de build.
Una route estática puede hidratar UI cliente.
Aumenta build. Elige un subconjunto y generación bajo demanda.
La duración debe derivarse del negocio y mecanismo de invalidación.
use cache o Suspense.connection() señala request-time sin otra Dynamic API.connection() y use cache?dynamic = force-static no es la solución recomendada con Cache Components?connection difiere al request; use cache reutiliza un resultado entre ejecuciones.Data fetching en Server Components aplica estas decisiones a fetch, bases de datos, SDKs, paralelización, errores y deduplicación.