En las arquitecturas tradicionales de Single Page Application (SPA) y en el Server-Side Rendering clásico (Next.js Pages Router), existía un problema estructural: la totalidad del código JavaScript de los componentes renderizados en el servidor debía descargarse en el navegador para su posterior hidratación.
A medida que la base de código crece, el tamaño del bundle de cliente se expande exponencialmente. El navegador se ve forzado a descargar, parsear y compilar megabytes de JavaScript incluso para áreas de interfaz totalmente estáticas: barras de navegación, pies de página, maquetación de artículos, así como librerías pesadas de servidor (procesadores Markdown, analizadores de sintaxis, formato de fechas, librerías matemáticas). Esto degrada de forma crítica las métricas Core Web Vitals, especialmente TTI e INP.
Un segundo problema reside en las cascadas de red (Network Waterfalls): un componente de cliente se monta, inicia una petición de datos a la API, espera la respuesta y luego monta un componente hijo que dispara a su vez otra petición.
React Server Components (RSC) en la arquitectura de Next.js App Router resuelven estas limitaciones:
En Next.js App Router, el entorno de ejecución se divide en dos dominios aislados pero profundamente coordinados: el Server Module Graph y el Client Module Graph.
Por defecto, todos los componentes son Server Components. No se requiere ninguna directiva especial para habilitar la ejecución en servidor. La separación entre ambos grafos ocurre en el Network Boundary mediante la directiva 'use client'.
Inspeccione los límites de serialización de React Flight y el impacto de 'use client' en el bundle
La directiva 'use client' se coloca en la hoja interactiva (el botón). Los datos y parsers pesados se ejecutan en el servidor y no entran en el bundle JS.
Es fundamental comprender que un componente marcado con 'use client' continúa renderizándose en el servidor durante la petición inicial para generar el HTML estático. La directiva 'use client' no significa "renderizar solo en navegador", sino "incorporar este módulo y todas sus importaciones hijas al bundle de JavaScript del cliente para su posterior hidratación".
El árbol de componentes de servidor se transmite a la red mediante el React Flight Payload. En lugar del código ejecutable del componente de cliente, el flujo incluye una Client Reference.
El usuario solicita una ruta. Next.js inicia la evaluación en el servidor del árbol de segmentos (Layouts, Pages).
El servidor procesa componentes asíncronos, consultando bases de datos y APIs externas concurrentemente.
React serializa el árbol: los elementos de servidor se convierten en estructura de datos y los de cliente en Client References con sus props serializadas.
El servidor entrega inmediatamente el HTML inicial para un Fast First Paint, transmitiendo en paralelo chunks de RSC Payload y límites de Suspense.
El navegador procesa el HTML, descarga los paquetes JS mínimos para componentes con 'use client' e hidrata islas interactivas sin recalcular la estructura de servidor.
¿Indicar la directiva 'use client' al inicio de un archivo significa que el componente se compila y ejecuta exclusivamente en el navegador?
Los Server Components se declaran como funciones estándar de React. Su propiedad más destacada es el soporte nativo para async/await directamente en el cuerpo de la función.
En Next.js 15+, los parámetros de ruta (params y searchParams) se tipan como Promise y deben resolverse explícitamente con await.
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function UserPage({ params, searchParams }: PageProps) {
// En Next.js 15+, el acceso a params y searchParams es asíncrono
const { id } = await params;
const query = await searchParams;
const activeTab = typeof query.tab === 'string' ? query.tab : 'overview';
// Consulta directa a la base de datos sin intermediarios REST
const user = await db.user.findUnique({
where: { id },
select: { id: true, name: true, email: true, role: true }
});
if (!user) {
notFound();
}
return (
<div className="p-8 max-w-2xl mx-auto">
<h1 className="text-2xl font-bold text-text-primary">{user.name}</h1>
<p className="text-text-muted">{user.email}</p>
<div className="mt-4 text-xs font-mono text-accent-cyan">
Pestaña activa: {activeTab}
</div>
</div>
);
}'use client')#La directiva 'use client' debe ubicarse en la primera línea del archivo antes de cualquier sentencia de importación o comentario.
Solo resulta necesaria cuando el componente requiere:
useState, useReducer, useEffect, useLayoutEffect.onClick, onChange, onSubmit, onKeyDown.window, document, localStorage, navigator, WebSockets.'use client';
import { useState, useTransition } from 'react';
interface CounterProps {
initialCount?: number;
}
export function Counter({ initialCount = 0 }: CounterProps) {
const [count, setCount] = useState(initialCount);
const [isPending, startTransition] = useTransition();
const handleIncrement = () => {
startTransition(() => {
setCount((prev) => prev + 1);
});
};
return (
<div className="flex items-center gap-4">
<button
type="button"
disabled={isPending}
onClick={handleIncrement}
className="px-4 py-2 rounded-lg bg-accent-violet hover:bg-accent-violet/90 text-white font-medium transition-colors disabled:opacity-50"
>
Incremento: {count}
</button>
{isPending && <span className="text-xs text-text-muted">Actualizando...</span>}
</div>
);
}'use client' frente a 'use server'#Una fuente habitual de confusión es creer que 'use server' convierte un componente en Server Component. No es así:
'use client': Establece el límite del grafo de módulos de cliente (Client Boundary).'use server': Declara una Server Action — función de servidor para mutación de datos (endpoint RPC) invocable mediante peticiones HTTP POST. ¡No define un componente gráfico de servidor!Cuando un componente interactivo de cliente (como un diálogo modal, acordeón o menú desplegable) necesita contener un componente de servidor con librerías pesadas, nunca importes el componente de servidor dentro del archivo de cliente.
En su lugar, emplea el Donut Pattern utilizando la prop children o slots JSX nombrados:
'use client';
import { useState } from 'react';
interface ModalDialogProps {
triggerLabel: string;
children: React.ReactNode; // ¡El Server Component se inyecta desde fuera!
}
export function ModalDialog({ triggerLabel, children }: ModalDialogProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button
type="button"
onClick={() => setIsOpen(true)}
className="px-3 py-1.5 rounded-md bg-bg-elevated border border-border text-sm"
>
{triggerLabel}
</button>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-bg-surface border border-border rounded-xl p-6 max-w-lg w-full">
<div className="flex justify-between items-center mb-4">
<h2 className="text-lg font-bold">Detalles</h2>
<button type="button" onClick={() => setIsOpen(false)}>✕</button>
</div>
{/* El slot de servidor se renderiza aquí, preservando su naturaleza de servidor */}
{children}
</div>
</div>
)}
</>
);
}import { ModalDialog } from '@/components/ModalDialog';
import { HeavyAnalyticsChart } from '@/components/HeavyAnalyticsChart'; // Server Component con dependencias pesadas
export default async function DashboardPage() {
return (
<main className="p-6">
<h1 className="text-xl font-bold mb-4">Analítica</h1>
{/* Donut Pattern: pasar Server Component como children al Client Component */}
<ModalDialog triggerLabel="Abrir informe">
<HeavyAnalyticsChart />
</ModalDialog>
</main>
);
}use())#En React 19 y Next.js 15+, a través de la frontera de serialización de props se pueden transferir objetos Promise no resueltos. Esto evita bloquear el TTFB del componente de servidor y permite resolver la promesa en el cliente mediante el hook use().
import { Suspense } from 'react';
import { ItemComments } from './ItemComments';
import { fetchCommentsPromise } from '@/lib/data';
export default async function ItemPage({
params
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
// Iniciar la obtención SIN await: la promesa se resuelve en segundo plano
const commentsPromise = fetchCommentsPromise(id);
return (
<div>
<h1>Ficha del producto #{id}</h1>
{/* Suspense muestra un esqueleto mientras se resuelve la promesa */}
<Suspense fallback={<p className="text-text-muted">Cargando comentarios...</p>}>
<ItemComments commentsPromise={commentsPromise} />
</Suspense>
</div>
);
}'use client';
import { use } from 'react';
interface Comment {
id: string;
author: string;
text: string;
}
export function ItemComments({
commentsPromise
}: {
commentsPromise: Promise<Comment[]>;
}) {
// El hook use() de React 19 desenvuelve la promesa directamente en el cliente
const comments = use(commentsPromise);
return (
<div className="space-y-3 mt-4">
<h3 className="font-semibold text-text-primary">Comentarios ({comments.length}):</h3>
{comments.map((comment) => (
<div key={comment.id} className="p-3 rounded-lg border border-border bg-bg-elevated/40">
<span className="font-medium text-accent-cyan text-sm">{comment.author}</span>
<p className="text-sm text-text-muted mt-1">{comment.text}</p>
</div>
))}
</div>
);
}Para evitar la importación accidental de utilidades de servidor en el cliente, instala el paquete server-only:
pnpm add server-onlyPara proteger entidades de base de datos sensibles contra filtraciones involuntarias, recurre a la React 19 Taint API:
import 'server-only';
import { experimental_taintObjectReference } from 'react';
import { db } from '@/lib/db';
export async function getUserAccount(id: string) {
const user = await db.user.findUnique({
where: { id }
});
if (user) {
// Si un desarrollador intenta enviar el objeto user completo a un Client Component,
// React bloqueará la ejecución con un error inmediato
experimental_taintObjectReference(
'Queda prohibido transferir el registro de usuario en crudo al cliente para evitar la exposición del hash de contraseña',
user
);
}
return user;
}¿Cuál de los siguientes tipos de datos SE PUEDE transferir con seguridad como prop desde un Server Component a un Client Component a través de React Flight?
Muchos programadores añaden 'use server' en la cabecera de componentes visuales creyendo que así forzarán su ejecución en servidor.
En App Router todos los componentes son Server Components por defecto. La directiva 'use server' se reserva exclusivamente para declarar Server Actions (funciones RPC asíncronas para mutar datos vía peticiones POST). Su uso en un componente visual ordinario generará un error en el compilador de Next.js.
Consultar registros en la base de datos y enviar la entidad completa a un Client Component serializará todos sus campos (incluidos passwordHash, stripeCustomerId, tokens internos) en el React Flight Payload, visible en texto plano en la pestaña Network de DevTools.
Solución: Mapea siempre las consultas a un DTO seguro que contenga exclusivamente las propiedades públicas requeridas por la interfaz.
// ❌ ANTIPATRÓN: Todo el registro se filtra al Flight Payload público
const user = await db.user.findUnique({ where: { id } });
return <UserBadge user={user} />;
// ✅ CORRECTO: Únicamente se serializan las propiedades públicas necesarias
return <UserBadge name={user.name} avatarUrl={user.avatarUrl} />;Importar directamente un archivo de Server Component en un módulo con 'use client' forzará a Turbopack a incluirlo en el grafo de cliente. Esto infla el bundle o provoca errores de compilación al intentar evaluar utilidades de servidor (BD, fs, env privadas) en el navegador. Utiliza siempre el Donut Pattern (children o slots).
Ejecutar una consulta asíncrona lenta en el layout.tsx principal (const settings = await getSlowSettings()) retrasa el Time to First Byte (TTFB) de toda la aplicación, mostrando una pantalla en blanco.
Solución: Desplaza las consultas de datos a niveles inferiores del árbol y aísla los componentes lentos con <Suspense fallback={<Skeleton />}>.