Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Server Component (predeterminado en App Router de Next.js) - cero JavaScript del cliente
// app/products/page.tsx
import { ProductFilters } from "./ProductFilters"; // componente de cliente
import { db } from "@/lib/db";
export default async function ProductsPage() {
// La obtención de datos ocurre en el servidor - sin fetch(), sin estado de carga, sin JavaScript del cliente
const products = await db.product.findMany({
orderBy: { createdAt: "desc" },
take: 50,
});
return (
<div>
<h1>Productos</h1>
{/* Solo este pequeño componente envía JS al cliente */}
<ProductFilters />
{/* Lista renderizada en el servidor - cero JavaScript del cliente */}
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} - ${p.price}
</li>
))}
</ul>
</div>
);
}
// ProductFilters.tsx - pequeño límite de "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">Electrónica</option>
<option value="clothing">Ropa</option>
</select>
);
}Cuándo usarlo: Para cada componente de forma predeterminada. Comienza con Server Components y solo añade "use client" cuando el componente necesita interactividad (useState, useEffect, manejadores de eventos, APIs del navegador).
// ---- ANTES: Página pesada con cliente - 245KB de JavaScript del cliente comprimido ----
// app/dashboard/page.tsx
"use client"; // ¡Toda la página es un componente de 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>Cargando...</div>;
if (!data) return <div>Error</div>;
return (
<div className="grid grid-cols-3 gap-6">
{/* Tarjetas de estadísticas - podrían ser HTML estático */}
<div className="p-4 bg-white rounded shadow">
<p className="text-gray-500">Ingresos</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 - necesita JavaScript del cliente pero solo 1 sección */}
<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 - podrían ser HTML estático */}
<div>
<h2>Pedidos Recientes</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>
);
}
// ---- DESPUÉS: Primero el servidor - 52KB de JavaScript del cliente comprimido (79% de reducción) ----
// app/dashboard/page.tsx - Server Component (predeterminado)
import { Suspense } from "react";
import { format } from "date-fns";
import { db } from "@/lib/db";
import { RevenueChart } from "./RevenueChart"; // solo componente de cliente
export default async function DashboardPage() {
// Obtención de datos del lado del servidor - cero JavaScript del cliente, sin estado de carga necesario
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">
{/* Tarjetas de estadísticas - HTML puro del servidor, cero JS */}
<StatsCard label="Ingresos" value={`$${stats.revenue.toLocaleString()}`} />
<StatsCard label="Pedidos" value={stats.orders.toString()} />
<StatsCard label="Clientes" value={stats.customers.toString()} />
{/* Gráfico - solo sección interactiva, envuelta en 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 del servidor, cero JS */}
<div>
<h2 className="font-semibold mb-2">Pedidos Recientes</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 puro del servidor - sin "use client", sin JavaScript enviado al 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 - límite de cliente más pequeño posible
"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>
);
}Lo que esto demuestra:
date-fns se ejecuta solo en el servidor: 4KB eliminado del bundle del cliente"use client" crea un límite - Todo lo importado en un archivo "use client" se convierte en parte del bundle del cliente, incluidas sus dependencias. Por eso es crítico mantener el límite lo más pequeño posible."use client" como hijo. El componente del servidor se renderiza en el servidor; el límite del componente del cliente se serializa e hidrata en el cliente.children. Este es el patrón de composición clave.Server Component envolviendo Client Component (patrón de composición):
// app/layout.tsx - Server Component
import { auth } from "@/lib/auth";
import { Sidebar } from "./Sidebar"; // componente de cliente
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await auth(); // Solo servidor: cero JavaScript del cliente
return (
<div className="flex">
{/* Pasar datos del servidor como props al componente de cliente */}
<Sidebar userName={user.name} role={user.role} />
<main>{children}</main>
</div>
);
}
// Sidebar.tsx - componente de cliente recibe datos del servidor como props serializables
"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)}>Toggle</button>
</nav>
);
}Pasar Server Components como children para evitar contaminación de límite del cliente:
// INCORRECTO: Hacer que todo el layout sea un componente de cliente
"use client";
function Layout() {
const [theme, setTheme] = useState("light");
return (
<div className={theme}>
<Header /> {/* Ahora un componente de cliente */}
<Sidebar /> {/* Ahora un componente de cliente */}
<MainContent /> {/* Ahora un componente de cliente - ¡todo JS enviado! */}
</div>
);
}
// CORRECTO: Aislar estado en un wrapper delgado
// 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 - cero JS */}
<Sidebar /> {/* Server Component - cero JS */}
{children}
</ThemeProvider>
);
}async. TypeScript infiere el tipo de retorno como Promise<JSX.Element>.React.ReactNode para la prop children en componentes wrapper de cliente.Marcar un layout completo como "use client" - Esto fuerza todos los componentes hijos a ser componentes de cliente, incluso si no necesitan interactividad. Un layout con "use client" puede añadir 100KB+ al bundle del cliente. Solución: Mantén layouts como Server Components. Extrae elementos interactivos (alternancia de tema, estado de navegación) en pequeños componentes de cliente.
Importar una librería solo del servidor en un componente de cliente - Si un componente de cliente importa prisma, fs u otros módulos solo del servidor, la compilación falla o toda la librería se agrupa para el cliente. Solución: Mueve importaciones solo del servidor a Server Components y pasa datos como props.
Pasar props no serializables - Las funciones, objetos Date, Map/Set e instancias de clase no pueden serializarse a través del límite servidor-cliente. Solución: Convierte a tipos serializables: usa strings ISO para fechas, objetos simples para instancias de clase, y Server Actions para funciones.
Sobre-dividir en demasiados componentes de cliente - Crear docenas de archivos "use client" diminutos para cada botón e entrada añade sobrecarga de hidratación y complejidad. Solución: Agrupa elementos interactivos relacionados en un único componente de cliente que represente una sección de UI lógica.
Asumir que los Server Components están en caché - Los Server Components se re-ejecutan en cada solicitud de forma predeterminada (renderizado dinámico). Solución: Usa export const revalidate = 3600 o generateStaticParams para renderizado estático cuando los datos no cambian por solicitud.
| Enfoque | Compensación |
|---|---|
| Server Components (predeterminado) | Cero JavaScript del cliente; no puedes usar hooks o APIs del navegador |
Componentes "use client" | Interactividad completa; añade al bundle del cliente |
| Importaciones dinámicas | Difiere carga; añade solicitud de red en primer uso |
| Arquitectura de islas (Astro) | Concepto similar; framework diferente |
| Generación estática (SSG) | Pre-renderizado en compilación; sin costo de servidor en tiempo de ejecución |
| Renderizado en borde | Server Components en el borde de CDN; latencia más baja |
Un Server Component se renderiza solo en el servidor. Su código, importaciones y dependencias nunca se envían al cliente. Esto significa que librerías como formateadores de fecha, clientes de base de datos y analizadores de markdown añaden cero bytes al bundle del cliente.
Solo cuando el componente necesita interactividad: useState, useEffect, manejadores de eventos o APIs del navegador. Por defecto usa Server Components y añade "use client" lo más tarde posible en el árbol de componentes.
No. Los componentes de cliente no pueden importar Server Components directamente. Sin embargo, los componentes de cliente pueden recibir 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 los componentes hijos se convierten en componentes de cliente, incluso si no necesitan interactividad. Esto puede añadir 100KB+ al bundle del cliente. Mantén layouts como Server Components y extrae elementos interactivos en pequeños componentes de cliente.
Solo tipos serializables: strings, números, booleanos, arrays, objetos simples y null. Las funciones, objetos Date, Map/Set e instancias de clase no pueden cruzar el límite. Usa strings ISO para fechas y Server Actions para funciones.
Obtén datos en el Server Component y pásalos como props serializables:
// page.tsx (Server Component)
const user = await auth();
return <Sidebar userName={user.name} role={user.role} />;Los Server Components transmiten HTML con datos reales en lugar de shells vacíos con spinners de carga. El navegador recibe contenido significativo en el primer byte, mejorando tanto TTFB como FCP. No se necesita un viaje redondo de fetch del lado del cliente.
No. Los Server Components se re-ejecutan en cada solicitud de forma predeterminada (renderizado dinámico). Usa export const revalidate = 3600 o generateStaticParams para renderizado estático cuando los datos no cambian por solicitud.
export default async function Page(): Promise<JSX.Element> {
const data = await fetchData();
return <div>{data.name}</div>;
}TypeScript infiere el tipo de retorno como Promise<JSX.Element> automáticamente. Usa React.ReactNode para children en componentes wrapper de cliente.
Sí. Crear docenas de archivos de cliente diminutos para cada botón e entrada añade sobrecarga de hidratación y complejidad. Agrupa elementos interactivos relacionados en un único componente de cliente que represente una sección de UI lógica.
Mantén la importación en un Server Component. Si un componente de cliente importa accidentalmente prisma, fs u otros módulos solo del servidor, la compilación falla o toda la librería se agrupa. Mueve importaciones solo del servidor a Server Components y pasa datos como props.
// layout.tsx - Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider>
<Header /> {/* Server Component */}
{children}
</ThemeProvider>
);
}ThemeProvider es "use client" pero recibe children renderizados en el servidor, manteniendo Header como un Server Component con cero JavaScript del cliente.
Revisado por Chris St. John·Última actualización: 19 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥