Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Server Component - sin JavaScript cliente para obtención de datos
// app/dashboard/page.tsx
import { db } from "@/lib/db";
export default async function DashboardPage() {
// Capturas PARALELAS - tiempo total = max(50ms, 120ms, 80ms) = 120ms
const [stats, orders, activity] = await Promise.all([
db.stats.findFirst(), // 50ms
db.order.findMany({ take: 10 }),// 120ms
db.activity.findMany({ take: 5 }), // 80ms
]);
return (
<div>
<StatsPanel data={stats} />
<OrderTable data={orders} />
<ActivityFeed data={activity} />
</div>
);
}
// Comparar con CASCADA - tiempo total = 50 + 120 + 80 = 250ms
// const stats = await db.stats.findFirst();
// const orders = await db.order.findMany({ take: 10 });
// const activity = await db.activity.findMany({ take: 5 });Cuándo usarlo: Para cada página que obtiene datos. Por defecto usa obtención de datos desde Server Component con Promise.all para solicitudes paralelas. Solo usa obtención del lado cliente (SWR, TanStack Query) cuando necesites actualizaciones en tiempo real o datos específicos del usuario que cambian después de la carga de página.
// ---- ANTES: Capturas en cascada - 1.8s total, 5 solicitudes secuenciales ----
// app/products/[id]/page.tsx
"use client";
import { useState, useEffect } from "react";
export default function ProductPage({ params }: { params: { id: string } }) {
const [product, setProduct] = useState(null);
const [reviews, setReviews] = useState([]);
const [related, setRelated] = useState([]);
const [seller, setSeller] = useState(null);
const [inventory, setInventory] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function load() {
// CASCADA: cada captura espera por la anterior
const productRes = await fetch(`/api/products/${params.id}`); // 200ms
const productData = await productRes.json();
setProduct(productData);
// Estas dependen del producto pero se obtienen secuencialmente
const reviewsRes = await fetch(`/api/reviews?productId=${params.id}`); // 300ms
setReviews(await reviewsRes.json());
const relatedRes = await fetch(`/api/products/related?category=${productData.category}`); // 400ms
setRelated(await relatedRes.json());
const sellerRes = await fetch(`/api/sellers/${productData.sellerId}`); // 150ms
setSeller(await sellerRes.json());
const inventoryRes = await fetch(`/api/inventory/${params.id}`); // 100ms
setInventory(await inventoryRes.json());
setLoading(false);
// Total: 200 + 300 + 400 + 150 + 100 = 1150ms de red + overhead de hidratación
}
load();
}, [params.id]);
if (loading) return <div>Cargando...</div>;
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>Vendido por: {seller.name}</p>
<p>En stock: {inventory.quantity}</p>
<ReviewList reviews={reviews} />
<RelatedProducts products={related} />
</div>
);
}
// ---- DESPUÉS: Obtención paralela desde Server Component - 400ms total (65% más rápido) ----
// app/products/[id]/page.tsx - Server Component
import { Suspense } from "react";
import { db } from "@/lib/db";
import { notFound } from "next/navigation";
import { ReviewList } from "./ReviewList";
import { RelatedProducts } from "./RelatedProducts";
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Primera captura: datos del producto (necesario para consultas de vendedor y relacionadas)
const product = await db.product.findUnique({
where: { id },
include: { seller: true }, // JOIN en lugar de captura separada - elimina N+1
});
if (!product) notFound();
// Segunda onda: Capturas PARALELAS para datos independientes
const [reviews, related, inventory] = await Promise.all([
db.review.findMany({
where: { productId: id },
orderBy: { createdAt: "desc" },
take: 20,
}),
db.product.findMany({
where: { category: product.category, id: { not: id } },
take: 6,
}),
db.inventory.findUnique({ where: { productId: id } }),
]);
// Total: 200ms (producto+vendedor) + max(300ms, 400ms, 100ms) = 200 + 400 = 600ms
// Pero con caché: producto en caché de visitas anteriores, así ~400ms
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>Vendido por: {product.seller.name}</p>
<p>En stock: {inventory?.quantity ?? 0}</p>
<Suspense fallback={<div className="h-64 animate-pulse" />}>
<ReviewList reviews={reviews} productId={id} />
</Suspense>
<Suspense fallback={<div className="h-48 animate-pulse" />}>
<RelatedProducts products={related} />
</Suspense>
</div>
);
}
// Aún más rápido: dividir en secciones con streaming
// app/products/[id]/page.tsx - Máximo paralelismo con streaming
export default async function ProductPageStreaming({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const product = await db.product.findUnique({
where: { id },
include: { seller: true },
});
if (!product) notFound();
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>Vendido por: {product.seller.name}</p>
{/* Cada sección hace streaming independientemente */}
<Suspense fallback={<InventorySkeleton />}>
<InventoryBadge productId={id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewSection productId={id} />
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedSection category={product.category} excludeId={id} />
</Suspense>
</div>
);
}
// Cada sección obtiene sus propios datos - todos hacen streaming en paralelo
async function InventoryBadge({ productId }: { productId: string }) {
const inventory = await db.inventory.findUnique({ where: { productId } });
return <p>En stock: {inventory?.quantity ?? 0}</p>;
}
async function ReviewSection({ productId }: { productId: string }) {
const reviews = await db.review.findMany({
where: { productId },
orderBy: { createdAt: "desc" },
take: 20,
});
return <ReviewList reviews={reviews} productId={productId} />;
}
async function RelatedSection({
category,
excludeId,
}: {
category: string;
excludeId: string;
}) {
const related = await db.product.findMany({
where: { category, id: { not: excludeId } },
take: 6,
});
return <RelatedProducts products={related} />;
}Lo que demuestra esto:
Promise.all = 600ms total (48% más rápido)include reemplaza captura de vendedor separada: elimina 1 ida y vuelta (prevención de N+1)Promise.all inicia todas las promesas simultáneamente y se resuelve cuando la más lenta se completa. El tiempo total es igual al tiempo de captura individual máximo, no la suma.include o select de Prisma con relaciones lo resuelve con una única consulta JOIN.Caché con revalidación:
// Captura con caché de Next.js
async function getProducts() {
const res = await fetch("https://api.example.com/products", {
next: {
revalidate: 3600, // Cachea por 1 hora
tags: ["products"], // Etiqueta para revalidación dirigida
},
});
return res.json();
}
// Revalida etiqueta de caché específica después de mutación
import { revalidateTag } from "next/cache";
async function createProduct(data: ProductData) {
await db.product.create({ data });
revalidateTag("products"); // Invalida caché de productos
}SWR para datos en tiempo real del lado cliente:
"use client";
import useSWR from "swr";
const fetcher = (url: string) => fetch(url).then((r) => r.json());
function LiveOrderCount() {
const { data, error, isLoading } = useSWR("/api/orders/count", fetcher, {
refreshInterval: 5000, // Sondea cada 5 segundos
dedupingInterval: 2000, // Deduplica solicitudes dentro de 2s
});
if (isLoading) return <span>...</span>;
if (error) return <span>Error</span>;
return <span>{data.count} órdenes</span>;
}Prefetch para navegación anticipada:
// Prefetch al pasar el mouse - datos listos cuando el usuario hace clic
import { useRouter } from "next/navigation";
function ProductCard({ product }: { product: Product }) {
const router = useRouter();
return (
<div
onMouseEnter={() => router.prefetch(`/products/${product.id}`)}
onClick={() => router.push(`/products/${product.id}`)}
>
{product.name}
</div>
);
}Prevención de N+1 con Prisma:
// MALO: N+1 - 1 consulta para órdenes + N consultas para clientes
const orders = await db.order.findMany();
const ordersWithCustomers = await Promise.all(
orders.map(async (order) => ({
...order,
customer: await db.customer.findUnique({ where: { id: order.customerId } }),
}))
);
// BUENO: Consulta única con JOIN
const orders = await db.order.findMany({
include: { customer: true },
});Promise.all preserva tipos de tupla: Promise.all([fetchA(), fetchB()]) devuelve [TypeA, TypeB].params se escriben como Promise<{ key: string }> en Next.js 15+.Awaits secuenciales que deberían ser paralelos - const a = await fetchA(); const b = await fetchB(); se ejecuta secuencialmente incluso si B no depende de A. Solución: Usa Promise.all([fetchA(), fetchB()]) para capturas independientes.
Obtener datos en componentes cliente cuando los Server Components funcionan - useEffect del lado cliente + fetch agrega latencia de hidratación, estados de carga y JS cliente. Solución: Por defecto usa obtención de datos desde Server Component. Solo usa obtención del lado cliente para datos que cambian después de la carga de página inicial.
Falta de manejo de errores en Promise.all - Si una promesa se rechaza, Promise.all se rechaza inmediatamente, perdiendo resultados de otras promesas. Solución: Usa Promise.allSettled cuando resultados parciales son aceptables, o envuelve cada promesa en un try-catch.
Obtención excesiva de datos - Seleccionar todas las columnas cuando solo necesitas id y name desperdicia ancho de banda y memoria. Solución: Usa Prisma select para obtener solo los campos que tu componente necesita.
Desajustes de clave de caché - Diferentes parámetros de consulta para los mismos datos lógicos crean entradas de caché separadas. Solución: Normaliza parámetros de consulta y usa patrones de clave de caché consistentes.
N+1 en páginas de lista - Renderizar una lista de elementos donde cada uno obtiene sus propios datos crea solicitudes N+1. Solución: Obtén todos los datos en el componente padre con una única consulta que incluya relaciones.
| Enfoque | Compensación |
|---|---|
| Server Component fetch | Sin JavaScript cliente; sin actualizaciones en tiempo real |
| SWR | Revalidación automática, dedup; overhead de JS cliente |
| TanStack Query | Caché poderoso, paginación; más pesado que SWR |
Promise.all | Paralelismo simple; manejo de errores de todo o nada |
Promise.allSettled | Resultados parciales en caso de fallo; manejo de resultado más complejo |
| Streaming con Suspense | Renderizado progresivo; requiere diseño de skeleton |
| GraphQL | Obtención de datos precisa; overhead de esquema |
Promise.all inicia todas las promesas simultáneamente.include o select con relaciones para resolverlo en una única consulta JOIN.// N+1: 1 + N consultas
const orders = await db.order.findMany();
for (const o of orders) {
o.customer = await db.customer.findUnique({ where: { id: o.customerId } });
}
// Arreglado: 1 consulta con JOIN
const orders = await db.order.findMany({ include: { customer: true } });<Suspense> hace streaming independientemente a medida que se resuelven sus datos.Promise.all se rechaza inmediatamente, descartando resultados de otras promesas.Promise.allSettled cuando resultados parciales son aceptables, o envuelve cada promesa en try-catch.const [statsResult, ordersResult] = await Promise.allSettled([
fetchStats(),
fetchOrders(),
]);
const stats = statsResult.status === "fulfilled" ? statsResult.value : null;import { useRouter } from "next/navigation";
function ProductCard({ product }: { product: Product }) {
const router = useRouter();
return (
<div
onMouseEnter={() => router.prefetch(`/products/${product.id}`)}
onClick={() => router.push(`/products/${product.id}`)}
>
{product.name}
</div>
);
}// TypeScript infiere [Stats, Order[], Activity[]]
const [stats, orders, activity] = await Promise.all([
fetchStats(), // Devuelve Promise<Stats>
fetchOrders(), // Devuelve Promise<Order[]>
fetchActivity(), // Devuelve Promise<Activity[]>
]);
// stats: Stats, orders: Order[], activity: Activity[]params es una Promise y debe ser awaited.export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
}id y name desperdicia ancho de banda y memoria.select para obtener solo los campos que tu componente renderiza.useSWR con la misma clave dentro del dedupingInterval reciben la misma respuesta de una única solicitud de red.dedupingInterval por defecto es 2 segundos.next: { revalidate: 3600 } en una llamada específica a fetch() cachea esa respuesta.export const revalidate = 3600 cachea toda la ruta y todos sus fetches.Revisado por Chris St. John·Última actualización: 10 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥