Busque em todas as páginas da documentação
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Server Component (padrão no Next.js App Router) - zero JS do cliente
// app/products/page.tsx
import { ProductFilters } from "./ProductFilters"; // componente do cliente
import { db } from "@/lib/db";
export default async function ProductsPage() {
// Busca de dados ocorre no servidor - sem fetch(), sem estado de carregamento, sem JS do cliente
const products = await db.product.findMany({
orderBy: { createdAt: "desc" },
take: 50,
});
return (
<div>
<h1>Produtos</h1>
{/* Apenas este pequeno componente envia JS para o cliente */}
<ProductFilters />
{/* Lista renderizada no servidor - zero JS do cliente */}
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} - ${p.price}
</li>
))}
</ul>
</div>
);
}
// ProductFilters.tsx - pequena fronteira "use client"
"use client";
import { useState } from "react";
export function ProductFilters() {
const [category, setCategory] = useState("all");
return (
<select value={category} onChange={(e) => setCategory(e.target.value)}>
<option value="all">Todos</option>
<option value="electronics">Eletrônicos</option>
<option value="clothing">Roupas</option>
</select>
);
}Quando usar isso: Para todos os componentes por padrão. Comece com Server Components e adicione "use client" apenas quando o componente precisar de interatividade (useState, useEffect, manipuladores de eventos, APIs do navegador).
// ---- ANTES: Página com muitos componentes do cliente - 245KB de JS do cliente compactado ----
// app/dashboard/page.tsx
"use client"; // A página inteira é um componente do cliente!
import { useState, useEffect } from "react";
import { format } from "date-fns";
import { BarChart, Bar, XAxis, YAxis } from "recharts";
interface DashboardData {
revenue: number;
orders: number;
customers: number;
recentOrders: Order[];
chartData: ChartPoint[];
}
export default function DashboardPage() {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/dashboard")
.then((res) => res.json())
.then((d) => {
setData(d);
setLoading(false);
});
}, []);
if (loading) return <div>Carregando...</div>;
if (!data) return <div>Erro</div>;
return (
<div className="grid grid-cols-3 gap-6">
{/* Cartões de estatísticas - poderiam ser HTML estático */}
<div className="p-4 bg-white rounded shadow">
<p className="text-gray-500">Receita</p>
<p className="text-2xl font-bold">${data.revenue.toLocaleString()}</p>
</div>
<div className="p-4 bg-white rounded shadow">
<p className="text-gray-500">Pedidos</p>
<p className="text-2xl font-bold">{data.orders}</p>
</div>
<div className="p-4 bg-white rounded shadow">
<p className="text-gray-500">Clientes</p>
<p className="text-2xl font-bold">{data.customers}</p>
</div>
{/* Gráfico - precisa de JS do cliente, mas apenas 1 seção */}
<div className="col-span-2">
<BarChart width={600} height={300} data={data.chartData}>
<XAxis dataKey="month" />
<YAxis />
<Bar dataKey="revenue" fill="#3b82f6" />
</BarChart>
</div>
{/* Lista de pedidos - poderia ser HTML estático */}
<div>
<h2>Pedidos Recentes</h2>
<ul>
{data.recentOrders.map((order) => (
<li key={order.id}>
{order.customer} - ${order.total}
<span className="text-gray-400 text-sm">
{format(new Date(order.date), "MMM dd")}
</span>
</li>
))}
</ul>
</div>
</div>
);
}
// ---- DEPOIS: Servidor primeiro - 52KB de JS do cliente compactado (redução de 79%) ----
// app/dashboard/page.tsx - Server Component (padrão)
import { Suspense } from "react";
import { format } from "date-fns";
import { db } from "@/lib/db";
import { RevenueChart } from "./RevenueChart"; // apenas componente do cliente
export default async function DashboardPage() {
// Busca de dados no lado do servidor - zero JS do cliente, sem necessidade de estado de carregamento
const [stats, recentOrders] = await Promise.all([
db.stats.findFirst(),
db.order.findMany({ orderBy: { date: "desc" }, take: 10 }),
]);
return (
<div className="grid grid-cols-3 gap-6">
{/* Cartões de estatísticas - HTML puro do servidor, zero JS */}
<StatsCard label="Receita" value={`$${stats.revenue.toLocaleString()}`} />
<StatsCard label="Pedidos" value={stats.orders.toString()} />
<StatsCard label="Clientes" value={stats.customers.toString()} />
{/* Gráfico - apenas seção interativa, envolvida em Suspense */}
<div className="col-span-2">
<Suspense fallback={<div className="h-72 animate-pulse bg-gray-100 rounded" />}>
<RevenueChart />
</Suspense>
</div>
{/* Lista de pedidos - HTML puro do servidor, zero JS */}
<div>
<h2 className="font-semibold mb-2">Pedidos Recentes</h2>
<ul className="space-y-2">
{recentOrders.map((order) => (
<li key={order.id} className="flex justify-between">
<span>
{order.customer} - ${order.total}
</span>
<span className="text-gray-400 text-sm">
{format(order.date, "MMM dd")}
</span>
</li>
))}
</ul>
</div>
</div>
);
}
// Componente puramente do servidor - sem "use client", sem JS enviado para o navegador
function StatsCard({ label, value }: { label: string; value: string }) {
return (
<div className="p-4 bg-white rounded shadow">
<p className="text-gray-500">{label}</p>
<p className="text-2xl font-bold">{value}</p>
</div>
);
}
// RevenueChart.tsx - a menor fronteira do cliente possível
"use client";
import dynamic from "next/dynamic";
import { useEffect, useState } from "react";
const BarChart = dynamic(
() => import("recharts").then((m) => m.BarChart),
{ ssr: false }
);
const Bar = dynamic(() => import("recharts").then((m) => m.Bar), { ssr: false });
const XAxis = dynamic(() => import("recharts").then((m) => m.XAxis), { ssr: false });
const YAxis = dynamic(() => import("recharts").then((m) => m.YAxis), { ssr: false });
export function RevenueChart() {
const [chartData, setChartData] = useState<ChartPoint[]>([]);
useEffect(() => {
fetch("/api/dashboard/chart")
.then((r) => r.json())
.then(setChartData);
}, []);
if (!chartData.length) return null;
return (
<BarChart width={600} height={300} data={chartData}>
<XAxis dataKey="month" />
<YAxis />
<Bar dataKey="revenue" fill="#3b82f6" />
</BarChart>
);
}O que isso demonstra:
date-fns é executado apenas no servidor: 4KB removidos do bundle do cliente"use client" cria uma fronteira - Tudo o que é importado em um arquivo "use client" se torna parte do bundle do cliente, incluindo suas dependências. É por isso que manter a fronteira o menor possível é crucial."use client" como filho. O componente do servidor renderiza no servidor; a fronteira do componente do cliente é serializada e hidratada no cliente.children. Este é o padrão de composição chave.Server Component envolvendo Client Component (padrão de composição):
// app/layout.tsx - Server Component
import { auth } from "@/lib/auth";
import { Sidebar } from "./Sidebar"; // componente do cliente
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await auth(); // Apenas servidor: zero JS do cliente
return (
<div className="flex">
{/* Passa dados do servidor como props para o componente do cliente */}
<Sidebar userName={user.name} role={user.role} />
<main>{children}</main>
</div>
);
}
// Sidebar.tsx - componente do cliente recebe dados do servidor como props serializáveis
"use client";
import { useState } from "react";
export function Sidebar({ userName, role }: { userName: string; role: string }) {
const [collapsed, setCollapsed] = useState(false);
return (
<nav className={collapsed ? "w-16" : "w-64"}>
<p>{userName} ({role})</p>
<button onClick={() => setCollapsed(!collapsed)}>Alternar</button>
</nav>
);
}Passando Server Components como children para evitar contaminação da fronteira do cliente:
// ERRADO: Tornando todo o layout um componente do cliente
"use client";
function Layout() {
const [theme, setTheme] = useState("light");
return (
<div className={theme}>
<Header /> {/* Agora um componente do cliente */}
<Sidebar /> {/* Agora um componente do cliente */}
<MainContent /> {/* Agora um componente do cliente - todo o JS enviado! */}
</div>
);
}
// CERTO: Isola o estado em um wrapper fino
// ThemeProvider.tsx
"use client";
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState("light");
return <div className={theme}>{children}</div>;
}
// layout.tsx - Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<Header /> {/* Server Component - zero JS */}
<Sidebar /> {/* Server Component - zero JS */}
{children}
</ThemeProvider>
);
}async. O TypeScript infere o tipo de retorno como Promise<JSX.Element>.React.ReactNode para a prop children em componentes wrapper do cliente.Marcar um layout inteiro como "use client" - Isso força todos os componentes filhos a serem componentes do cliente, mesmo que não precisem de interatividade. Um layout com "use client" pode adicionar 100KB+ ao bundle do cliente. Correção: Mantenha os layouts como Server Components. Extraia elementos interativos (alternância de tema, estado de navegação) para pequenos componentes do cliente.
Importar uma biblioteca apenas para servidor em um componente do cliente - Se um componente do cliente importar prisma, fs ou outros módulos apenas para servidor, a compilação falha ou toda a biblioteca é empacotada para o cliente. Correção: Mova imports apenas para servidor para Server Components e passe os dados como props.
Passar props não serializáveis - Funções, objetos Date, Map/Set e instâncias de classe não podem ser serializados através da fronteira servidor-cliente. Correção: Converta para tipos serializáveis: use strings ISO para datas, objetos simples para instâncias de classe e Server Actions para funções.
Dividir excessivamente em muitos componentes do cliente - Criar dezenas de pequenos arquivos "use client" para cada botão e input adiciona sobrecarga de hidratação e complexidade. Correção: Agrupe elementos interativos relacionados em um único componente do cliente que representa uma seção lógica da UI.
Assumir que Server Components são cacheados - Server Components reexecutam a cada requisição por padrão (renderização dinâmica). Correção: Use export const revalidate = 3600 ou generateStaticParams para renderização estática quando os dados não mudam a cada requisição.
| Abordagem | Trade-off |
|---|---|
| Server Components (padrão) | Zero JS do cliente; não pode usar hooks ou APIs do navegador |
Componentes "use client" | Interatividade total; adiciona ao bundle do cliente |
| Imports dinâmicos | Adia o carregamento; adiciona requisição de rede no primeiro uso |
| Arquitetura de Ilhas (Astro) | Conceito similar; framework diferente |
| Geração estática (SSG) | Pré-renderizado na compilação; sem custo de servidor em tempo de execução |
| Renderização Edge | Server Components na borda da CDN; menor latência |
Um Server Component renderiza apenas no servidor. Seu código, imports e dependências nunca são enviados ao cliente. Isso significa que bibliotecas como formatadores de data, clientes de banco de dados e parsers de markdown adicionam zero bytes ao bundle do cliente.
Apenas quando o componente precisa de interatividade: useState, useEffect, manipuladores de eventos ou APIs do navegador. Prefira Server Components e adicione "use client" o mais tarde possível na árvore de componentes.
Não. Componentes do cliente não podem importar Server Components diretamente. No entanto, componentes do cliente podem receber Server Components como props children:
// ThemeProvider.tsx - "use client"
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState("light");
return <div className={theme}>{children}</div>;
}Todos os componentes filhos se tornam componentes do cliente, mesmo que não precisem de interatividade. Isso pode adicionar 100KB+ ao bundle do cliente. Mantenha os layouts como Server Components e extraia elementos interativos para pequenos componentes do cliente.
Apenas tipos serializáveis: strings, números, booleanos, arrays, objetos simples e null. Funções, objetos Date, Map/Set e instâncias de classe não podem cruzar a fronteira. Use strings ISO para datas e Server Actions para funções.
Busque dados no Server Component e passe-os como props serializáveis:
// page.tsx (Server Component)
const user = await auth();
return <Sidebar userName={user.name} role={user.role} />;Server Components transmitem HTML com dados reais em vez de shells vazios com spinners de carregamento. O navegador recebe conteúdo significativo no primeiro byte, melhorando tanto o TTFB quanto o FCP. Nenhuma viagem de ida e volta de fetch no cliente é necessária.
Não. Server Components reexecutam a cada requisição por padrão (renderização dinâmica). Use export const revalidate = 3600 ou generateStaticParams para renderização estática quando os dados não mudam a cada requisição.
export default async function Page(): Promise<JSX.Element> {
const data = await fetchData();
return <div>{data.name}</div>;
}O TypeScript infere o tipo de retorno como Promise<JSX.Element> automaticamente. Use React.ReactNode para children em componentes wrapper do cliente.
Sim. Criar dezenas de pequenos arquivos do cliente para cada botão e input adiciona sobrecarga de hidratação e complexidade. Agrupe elementos interativos relacionados em um único componente do cliente que representa uma seção lógica da UI.
Mantenha o import em um Server Component. Se um componente do cliente importar acidentalmente prisma, fs ou outros módulos apenas para servidor, a compilação falha ou toda a biblioteca é empacotada. Mova imports apenas para servidor para Server Components e passe os dados como props.
// layout.tsx - Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<Header /> {/* Server Component */}
{children}
</ThemeProvider>
);
}ThemeProvider é "use client", mas recebe children renderizados no servidor, mantendo Header como um Server Component com zero JS do cliente.
Revisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥