Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Server Component - zero JS do cliente para busca de dados
// app/dashboard/page.tsx
import { db } from "@/lib/db";
export default async function DashboardPage() {
// Buscas PARALELAS - tempo 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>
);
}
// Compare com WATERFALL - tempo 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 });Quando usar isso: Para toda página que busca dados. Use por padrão a busca em Server Component com Promise.all para requisições paralelas. Use busca do lado do cliente (SWR, TanStack Query) apenas quando precisar de atualizações em tempo real ou dados específicos do usuário que mudam após o carregamento da página.
// ---- ANTES: Buscas em Waterfall - 1.8s total, 5 requisições sequenciais ----
// 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() {
// WATERFALL: cada busca espera a anterior
const productRes = await fetch(`/api/products/${params.id}`); // 200ms
const productData = await productRes.json();
setProduct(productData);
// Estas dependem do produto mas são buscadas sequencialmente
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: 1150ms de rede + sobrecarga de hidratação
}
load();
}, [params.id]);
if (loading) return <div>Carregando...</div>;
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>Vendido por: {seller.name}</p>
<p>Em estoque: {inventory.quantity}</p>
<ReviewList reviews={reviews} />
<RelatedProducts products={related} />
</div>
);
}
// ---- DEPOIS: Busca paralela em Server Component - 400ms total (65% mais 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;
// Primeira busca: dados do produto (necessário para as consultas de vendedor e relacionados)
const product = await db.product.findUnique({
where: { id },
include: { seller: true }, // JOIN em vez de busca separada - elimina N+1
});
if (!product) notFound();
// Segunda onda: buscas PARALELAS para dados independentes
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 (produto+vendedor) + max(300ms, 400ms, 100ms) = 200 + 400 = 600ms
// Mas com cache: produto em cache de visitas anteriores, então ~400ms
return (
<div>
<h1>{product.name}</h1>
<p>${product.price}</p>
<p>Vendido por: {product.seller.name}</p>
<p>Em estoque: {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>
);
}
// Ainda mais rápido: dividido em seções de streaming
// app/products/[id]/page.tsx - Paralelismo máximo com 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 seção transmite independentemente */}
<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 seção busca seus próprios dados - todos transmitem em paralelo
async function InventoryBadge({ productId }: { productId: string }) {
const inventory = await db.inventory.findUnique({ where: { productId } });
return <p>Em estoque: {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} />;
}O que isso demonstra:
Promise.all = 600ms total (48% mais rápido)include do Prisma substitui a busca de vendedor separada: elimina 1 viagem de ida e volta (prevenção de N+1)Promise.all inicia todas as Promises simultaneamente e resolve quando a mais lenta é concluída. O tempo total é igual ao tempo máximo de busca individual, não à soma.include ou select do Prisma com relações resolve isso com uma única consulta JOIN.Cache com revalidação:
// Busca com cache do Next.js
async function getProducts() {
const res = await fetch("https://api.example.com/products", {
next: {
revalidate: 3600, // Cache por 1 hora
tags: ["products"], // Tag para revalidação direcionada
},
});
return res.json();
}
// Revalida tag de cache específica após mutação
import { revalidateTag } from "next/cache";
async function createProduct(data: ProductData) {
await db.product.create({ data });
revalidateTag("products"); // Invalida o cache de produtos
}SWR para dados em tempo real do lado do 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, // Consulta a cada 5 segundos
dedupingInterval: 2000, // Deduplica requisições em até 2s
});
if (isLoading) return <span>...</span>;
if (error) return <span>Erro</span>;
return <span>{data.count} pedidos</span>;
}Prefetching para navegação antecipada:
// Prefetch ao passar o mouse - dados prontos quando o usuário clica
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>
);
}Prevenindo N+1 com Prisma:
// RUIM: N+1 - 1 consulta para pedidos + 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 } }),
}))
);
// BOM: Consulta única com JOIN
const orders = await db.order.findMany({
include: { customer: true },
});Promise.all preserva tipos de tupla: Promise.all([fetchA(), fetchB()]) retorna [TypeA, TypeB].params são tipadas como Promise<{ key: string }> no Next.js 15+.awaits sequenciais que deveriam ser paralelos - const a = await fetchA(); const b = await fetchB(); executa sequencialmente mesmo que B não dependa de A. Correção: Use Promise.all([fetchA(), fetchB()]) para buscas independentes.
Buscar em componentes do cliente quando Server Components funcionam - useEffect + fetch do lado do cliente adiciona latência de hidratação, estados de carregamento e JS do cliente. Correção: Use por padrão a busca de dados em Server Components. Use busca do lado do cliente apenas para dados que mudam após o carregamento inicial da página.
Falta de tratamento de erro em Promise.all - Se uma Promise rejeitar, Promise.all rejeita imediatamente, perdendo resultados de outras Promises. Correção: Use Promise.allSettled quando resultados parciais são aceitáveis, ou envolva cada Promise em um try-catch.
Excesso de busca de dados - Selecionar todas as colunas quando você só precisa de id e name desperdiça largura de banda e memória. Correção: Use select do Prisma para buscar apenas os campos que seu componente renderiza.
Incompatibilidade de chaves de cache - Diferentes parâmetros de consulta para os mesmos dados lógicos criam entradas de cache separadas. Correção: Normalize os parâmetros de consulta e use padrões de chaves de cache consistentes.
N+1 em páginas de lista - Renderizar uma lista de itens que cada um busca seus próprios dados cria N+1 requisições. Correção: Busque todos os dados no componente pai com uma única consulta que inclua relações.
| Abordagem | Compromisso |
|---|---|
| Fetch em Server Component | Zero JS do cliente; sem atualizações em tempo real |
| SWR | Revalidação automática, dedup; sobrecarga de JS do cliente |
| TanStack Query | Cache poderoso, paginação; mais pesado que SWR |
Promise.all | Paralelismo simples; tratamento de erro tudo-ou-nada |
Promise.allSettled | Resultados parciais em caso de falha; tratamento de resultado mais complexo |
| Streaming com Suspense | Renderização progressiva; requer design de esqueleto |
| GraphQL | Busca de dados precisa; sobrecarga de esquema |
Promise.all inicia todas as Promises simultaneamente.include ou select do Prisma com relações para resolver isso em uma ú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 } });
}
// Corrigido: 1 consulta com JOIN
const orders = await db.order.findMany({ include: { customer: true } });<Suspense> transmite independentemente à medida que seus dados são resolvidos.Promise.all rejeita imediatamente, descartando resultados de outras promises.Promise.allSettled quando resultados parciais são aceitáveis, ou envolva cada promise em 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 infere [Stats, Order[], Activity[]]
const [stats, orders, activity] = await Promise.all([
fetchStats(), // Retorna Promise<Stats>
fetchOrders(), // Retorna Promise<Order[]>
fetchActivity(), // Retorna Promise<Activity[]>
]);
// stats: Stats, orders: Order[], activity: Activity[]params é uma Promise e deve ser aguardado (await).export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
}id e name desperdiça largura de banda e memória.select do Prisma para buscar apenas os campos que seu componente renderiza.useSWR com a mesma chave dentro do dedupingInterval recebem a mesma resposta de uma única requisição de rede.dedupingInterval padrão é de 2 segundos.next: { revalidate: 3600 } em uma chamada fetch() específica armazena em cache essa única resposta.export const revalidate = 3600 armazena em cache a rota inteira e todas as suas buscas.Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥