Inicio / Artículos / Componentes de servidor de React: La arquitectura detrás del renderizado sin paquetes

Componentes de servidor de React: La arquitectura detrás del renderizado sin paquetes

Comprenda la lógica detrás de los React Server Components, desde el tamaño del paquete y los problemas relacionados con el flujo de trabajo hasta el límite entre servidor y cliente y las cargas útiles de RSC.

3487 palabras

Si has dedicado tiempo a intentar comprender los React Server Components y al final te sientes más confundido que al principio, eso no significa que te falte algo obvio. Significa que las explicaciones que encontraste formaban parte del problema. Durante un par de años, la descripción oficial presentaba los Server Components como “componentes que se renderizan en el servidor”, una frase que suena sospechosamente similar a lo que ya hacían getServerSideProps o el renderizado en servidor clásico en Next.js. Luego surgió la afirmación de que estos componentes “envían cero JavaScript al cliente”, lo cual suena más a un truco de rendimiento que a una nueva forma de estructurar una aplicación. Finalmente llegó el App Router, y de repente todos los archivos en un proyecto Next.js pasaron a ser por defecto Server Components, a menos que se incluyera una cadena especial al principio del archivo.

La confusión no fue un fallo por su parte. Ocurre porque se estaba describiendo un paradigma verdaderamente nuevo con vocabulario tomado de uno más antiguo. Esta guía tiene como objetivo cerrar esa brecha. Para cuando termine de leerla, debería comprender no solo la mecánica de los Componentes del Servidor, sino también el razonamiento que los convierte en una forma completamente distinta de pensar las aplicaciones React.

El problema que realmente resuelven los RSC

Antes de adentrarnos en cómo funcionan los Componentes del Servidor, es útil entender qué llevó a React a inventarlos en primer lugar. El problema nunca fue que el renderizado del lado del servidor fuera demasiado lento. El verdadero problema es que el modelo de componentes de React se basaba en la suposición de que todo ocurre en el navegador, incluso en casos en los que esa suposición generaba costos innecesarios.

La trampa del tamaño del paquete

In the classic React model, every component you author turns into JavaScript that gets shipped to the browser, no exceptions. It makes no difference whether that component just renders some static markdown, does a simple date calculation, or hits a database. As long as it lives somewhere in your component tree, its code lives in your bundle too.

That creates an unforgiving trade-off. Adding more components inflates your JavaScript payload. A heavier payload pushes out your Time to Interactive. In practice, you end up delivering code to browsers that never had any reason to execute it.

The Waterfall Problem

Antes de Server Components, cualquier componente que necesitara datos tenía que obtenerlos después de llegar al cliente. El patrón típico consistía en renderizar una carcasa temporal, iniciar un useEffect, esperar una respuesta y solo entonces mostrar el contenido real. Si ese contenido incluía un componente anidado con sus propias necesidades de datos, se repetía el mismo ciclo: otro efecto, otra espera, otro retraso añadido al anterior. Esta reacción en cadena es la cascada de obtención de datos, y explica por qué muchas aplicaciones React parecen lentas incluso cuando sus APIs backend responden rápidamente.

Una solución temporal fue trasladar la obtención de datos al nivel de la ruta utilizando herramientas como getServerSideProps o cargadores de rutas, pero eso conllevó un costo: afectó la naturaleza autónoma de los componentes. De repente, la página tenía que conocer datos que conceptualmente pertenecían a sus hijos, y los componentes dejaron de ser unidades independientes.

El costo de la hidratación

La renderización tradicional del lado del servidor funciona generando HTML en el servidor, enviándolo y luego volviendo a renderizar toda la aplicación en JavaScript en el cliente para que se vuelva interactiva. Eso significa que cada componente, incluidos aquellos que nunca necesitarán comportamiento del lado del cliente, aún debe pasar por el proceso de hidratación. En efecto, se paga un costo por la interactividad incluso para contenido que es completamente estático.

Los React Server Components abordan estos tres problemas a nivel de arquitectura, y no como una mejora incremental del rendimiento.

El modelo mental: los componentes de servidor no son “SSR 2.0”

Esta es la idea central en la que se basa toda esta guía: los componentes de servidor no son una forma de renderizar páginas. Son una categoría distinta de componentes.

React clásico solo reconocía un tipo de componente. Este se ejecutaba en el navegador, podía almacenar estado, ejecutar efectos y responder a eventos. Todo su ciclo de vida, desde la creación hasta la desmontaje, ocurría en el lado del cliente.

Los React Server Components añaden una segunda categoría. Un componente servidor se ejecuta en el servidor. Puede acceder libremente al sistema de archivos, consultar una base de datos directamente o utilizar una biblioteca que solo funciona dentro de Node.js. Lo que no puede hacer es usar useState, useEffect ni adjuntar manejadores de eventos, ya que simplemente nunca se ejecuta en el navegador. No existe un paso de hidratación para él. Su código ni siquiera se transmite al cliente como JavaScript.

Una forma útil de visualizarlo es pensar que el árbol de componentes de React ahora es compuesto: ciertas ramas se generan en el servidor y otras en el navegador. Las ramas del lado del servidor se renderizan una sola vez, durante la solicitud, y lo que generan se envía al cliente en forma de elementos React serializados. Las ramas del lado del cliente se renderizan en el navegador como de costumbre y mantienen todas las capacidades interactivas a las que estás acostumbrado.

Este es un mecanismo diferente al SSR. El rendering del lado del servidor procesa toda la aplicación en el servidor para convertirla en HTML y luego vuelve a cargar todo eso en el navegador posteriormente. En cambio, RSC solo renderiza componentes específicos en el servidor y nunca transmite su código subyacente al navegador.

¿Qué se envía realmente al cliente?

Un componente del servidor no genera HTML cuando se renderiza. En su lugar, produce una descripción serializada de su salida conocida como carga útil de RSC: un flujo de instrucciones similares a JSON que indica a React en el lado del cliente cómo armar el árbol que necesita mostrar.

A continuación se muestra la secuencia de eventos que ocurren en segundo plano cuando se solicita una página que contiene componentes del servidor:

  1. React renderiza los componentes del servidor en el servidor.
  • Para cada componente del servidor, emite la salida renderizada real: los elementos generados, no el código fuente que los creó.
  • Para cada componente del cliente, en cambio emite una referencia: un marcador que indica dónde debe montarse ese componente, junto con las propiedades que necesita.
  • El servidor transmite todo este contenido al navegador de forma continua.
  • El entorno de ejecución de React del navegador analiza ese contenido y construye el árbol resultante.
  • Luego, los componentes del cliente descargan su código e se hidratan. Los componentes del servidor, al ya haber sido renderizados, no requieren ningún paso de hidratación.
  • La conclusión clave es que el código de los componentes del servidor nunca llega al paquete de JavaScript enviado a los navegadores. Imagina un componente MarkdownRenderer construido sobre una biblioteca de análisis de Markdown de 200 KB. Si ese componente es un componente del servidor, toda esa biblioteca de 200 KB permanece en el servidor. El navegador solo ve la estructura resultante generada por el analizador, nunca al propio analizador.

    Esa es la sustancia detrás de la afirmación sobre un tamaño cero del paquete, y no se trata de una técnica de optimización menor. Representa un cambio real en lo que implica realmente construir una aplicación React.

    El límite entre servidor y cliente

    Dentro del App Router, cada archivo se considera un Componente Servidor a menos que se indique lo contrario. Esto no es solo un valor por defecto estilístico, sino una suposición estructural integrada en el framework, que refleja la idea de que la mayor parte de tu interfaz no necesita ejecutarse en el navegador en absoluto.

    Por el contrario, un Componente Cliente es código destinado a ejecutarse en la máquina del usuario. Se marca un archivo de esta manera colocando la directiva "use client" en su parte superior. Esto no es simplemente texto informativo, sino que funciona como un límite estricto. Indica al compilador de React que todo lo definido en ese archivo, junto con cualquier elemento que se incluya mediante importaciones, debe ser empaquetado y enviado al navegador.

    La regla que mantiene coherente todo este sistema es la siguiente: los Componentes de Servidor pueden importar y renderizar componentes de Cliente libremente, pero lo inverso nunca está permitido; un componente de Cliente no puede importar un componente de Servidor.

    A primera vista parece algo contradictorio. ¿No debería el código del lado del servidor ser utilizable desde cualquier lugar, incluidos los archivos del cliente? En realidad no, si se analiza cómo se mueven realmente los datos:

    • Primero se ejecuta un componente de Servidor, con acceso directo a la base de datos y a los recursos del backend. Este obtiene los datos que necesita y crea un diseño. En algún punto de ese diseño podría renderizarse algo como <UserProfile />, lo cual requiere interactividad; por eso esa parte se escribe como un componente de Cliente. El componente de Servidor padre transmite los datos del usuario obtenidos como propiedades.
  • El Componente de Cliente simplemente consume esas propiedades y renderiza las partes interactivas. No tiene forma de acceder al servidor e importar un Componente de Servidor, porque para cuando se ejecuta en el navegador, la ejecución del lado del servidor ya ha finalizado hace tiempo. No queda ningún proceso de servidor al que poder hacer llamadas.
  • Por eso este tipo de arreglo no puede funcionar:

    // ❌ Impossible: Client Component importing Server Component
    'use client';
    import { ServerDataFetcher } from './ServerDataFetcher'; // This breaks!
    export function ClientWidget() {
      return (
        <div>
          <ServerDataFetcher /> {/* Cannot render a server component here */}
        </div>
      );
    }
    

    Pero estructurar las cosas de la otra manera es perfectamente válido:

    // ✅ Correct: Server Component importing Client Component
    // This is a Server Component (no 'use client')
    import { ClientWidget } from './ClientWidget';
    async function ServerDataFetcher() {
      const data = await db.query('SELECT * FROM posts');
    
      return (
        <div>
          <h1>Latest Posts</h1>
          {data.map(post => (
            <ClientWidget key={post.id} post={post} />
          ))}
        </div>
      );
    }
    

    El patrón es sencillo: el Componente de Servidor se encarga de obtener los datos y del renderizado estructural, y luego envía la información a los componentes interactivos que se encuentran en las hojas del árbol. Esos componentes de hoja se ocupan de los clics, los envíos de formularios y las animaciones. La arquitectura resultante es, en esencia, un árbol renderizado en el servidor, con zonas de interactividad dispersas en sus extremos.

    Componentes Asíncronos: La Revolución en la Obtención de Datos

    React clásico nunca permitió que las funciones de componente fueran asíncronas. Escribir await directamente dentro del cuerpo de un componente no era una opción; uno se veía obligado a utilizar useEffect y gestionar manualmente el estado de carga.

    Los componentes del servidor rompen esa limitación: pueden ser async. Aunque parece una adición sintáctica menor, transforma significativamente la arquitectura subyacente.

    // ✅ Server Component: Direct data access, no useEffect
    async function BlogPostList() {
      // This runs on the server. No fetch call in the browser.
      const posts = await db.post.findMany({
        orderBy: { createdAt: 'desc' },
        take: 10
      });
    
      return (
        <ul>
          {posts.map(post => (
            <li key={post.id}>
              <h2>{post.title}</h2>
              <p>{post.excerpt}</p>
            </li>
          ))}
        </ul>
      );
    }
    

    Fíjese en todo lo que falta aquí. No hay useState para rastrear una bandera de carga, ni useEffect que inicie una solicitud, ni pantalla esqueleto creada manualmente. El componente simplemente mapea directamente los registros de la base de datos a elementos de React. Ahora las solicitudes se realizan por componente y no por ruta, y ocurren exclusivamente en el servidor, nunca en el navegador.

    Gracias a esto, el problema de las cataratas desaparece: el servidor puede procesar cada componente asíncrono de forma concurrente antes incluso de enviar una respuesta al cliente. Las llamadas a la base de datos se ejecutan dentro del mismo centro de datos que tu backend, por lo que la latencia se mide en microsegundos y no en milisegundos, como ocurre típicamente con las conexiones móviles.

    La directiva “use client”: cuándo y por qué

    Agregar "use client" indica que un archivo pertenece al entorno de ejecución del navegador. Lo necesitas siempre que un componente interactúe con algo inherentemente relacionado con el lado del cliente:

    • Estados locales mediante useState o useReducer
    • Ganchos de ciclo de vida como useEffect o useLayoutEffect
    • Manejo de eventos, como onClick o onSubmit
  • APIs directas del navegador — localStorage, window, document
  • Ganchos vinculados al contexto del navegador, incluyendo ciertos usos de useRouter o elementos como useMediaQuery
  • El error común es colocar esta directiva demasiado arriba en el árbol de componentes. Los desarrolladores a menudo convierten toda una página en un componente cliente solo porque un botón incrustado necesita interactividad.

    // ❌ Bad: Making the whole page client-side for one interactive element
    'use client';
    import { useState } from 'react';
    import { HeroSection } from './HeroSection'; // Static, could be server
    import { LikeButton } from './LikeButton';   // Interactive, needs client
    export default function Page() {
      return (
        <div>
          <HeroSection />
          <LikeButton />
        </div>
      );
    }
    

    En ese fragmento, HeroSection es un marcado puramente estático que podría permanecer renderizado en el servidor sin problemas. Pero como "use client" se declaró en el nivel padre, todas las importaciones que se encuentran debajo de él — incluida esa sección estática — se empaquetan y se envían al navegador de todos modos.

    La solución: Desplazar "use client" hacia abajo en el árbol

    La solución consiste en mantener la página misma como un componente del servidor y tratar la parte interactiva como un nodo hoja que se importa en ella.

    // ✅ Good: Page is server, only LikeButton is client
    // page.tsx (Server Component by default)
    import { HeroSection } from './HeroSection';
    import { LikeButton } from './LikeButton';
    export default function Page() {
      return (
        <div>
          <HeroSection />
          <LikeButton postId="123" />
        </div>
      );
    }
    
    // LikeButton.tsx
    'use client';
    import { useState } from 'react';
    export function LikeButton({ postId }) {
      const [liked, setLiked] = useState(false);
    
      return (
        <button onClick={() => setLiked(!liked)}>
          {liked ? '❤️' : '🤍'}
        </button>
      );
    }
    

    Con esta estructura, ni HeroSection ni Page se envían nunca al navegador en formato JavaScript. Solo LikeButton, junto con su llamada a useState, es lo que se envía. Este es el mecanismo práctico detrás del objetivo de un tamaño cero para los paquetes: dejar la mayor parte posible del árbol de componentes en el servidor y reservar el lado del cliente para los nodos que realmente necesitan interactividad.

    Entrelazado: El patrón que hace poderoso a RSC

    La técnica que confiere a RSC su verdadera fuerza es la intercalación: anidar Componentes Servidor dentro de Componentes Cliente, pasar datos provenientes del servidor como propiedades y permitir que esos Componentes Cliente expongan ranuras children que puedan contener más Componentes Servidor.

    // Layout.tsx (Server Component)
    import { Sidebar } from './Sidebar';
    import { AnalyticsProvider } from './AnalyticsProvider';
    export default async function DashboardLayout({ children }) {
      // Fetch user data on the server
      const user = await getCurrentUser();
      const permissions = await getUserPermissions(user.id);
    
      return (
        <div className="dashboard">
          <Sidebar user={user} permissions={permissions} />
    
          {/* AnalyticsProvider is a Client Component */}
          <AnalyticsProvider userId={user.id}>
            {/* children here can be a Server Component page */}
            <main>{children}</main>
          </AnalyticsProvider>
        </div>
      );
    }
    
    // AnalyticsProvider.tsx
    'use client';
    import { createContext, useContext } from 'react';
    const AnalyticsContext = createContext(null);
    export function AnalyticsProvider({ userId, children }) {
      // Client-side analytics initialization
      useEffect(() => {
        analytics.identify(userId);
      }, [userId]);
    
      return (
        <AnalyticsContext.Provider value={{ userId }}>
          {children}
        </AnalyticsContext.Provider>
      );
    }
    

    Aquí, DashboardLayout se ejecuta en el servidor y consulta la base de datos directamente. Envía esos datos a Sidebar, que puede ser un Componente Servidor o Cliente según sus necesidades. También envuelve el resto de la página en AnalyticsProvider, un Componente Cliente necesario en el navegador para inicializar una biblioteca de análisis de terceros.

    El detalle importante es la propiedad children. Todo lo que se renderiza dentro de AnalyticsProvider no tiene por qué convertirse en código del cliente solo porque esté anidado allí. React transmite la salida ya renderizada del Componente Servidor a través del Componente Cliente como children; el Componente Cliente simplemente actúa como un contenedor o límite, no como un convertidor que obligue a todo lo que está dentro de él a ejecutarse en el lado del cliente.

    Este patrón es exactamente cómo se construyen las interfaces de usuario híbridas: contenido renderizado en el servidor anidado dentro de contenedores exclusivos para el cliente, justo donde se requiere realmente la interactividad.

    La carga útil de RSC: un vistazo al funcionamiento interno

    El renderizado del Componente Servidor no produce HTML directamente. En su lugar, React emite la carga útil de RSC, un flujo binario que conceptualmente se asemeja a esto:

    1:I["node_modules/react/jsx-runtime.js", "jsx"]
    2:I["./components/ClientWidget.js", "default"]
    0:["quot;, "div", null, {"children": [
      ["quot;, "h1", null, {"children": "Latest Posts"}],
      ["quot;, "@2", null, {"postId": "123", "title": "Hello World"}]
    ]}]
    

    Cada línea de ese flujo es una instrucción. El símbolo $ marca un elemento de React. I indica una importación de un módulo de componente cliente. Una referencia como @2 apunta a la segunda importación; en este caso, ClientWidget. Obsérvese que el servidor ya ha renderizado completamente la etiqueta h1, pero en el caso de ClientWidget solo ha reenviado las propiedades y señalado dónde se encuentra su código, sin renderizarlo él mismo.

    Una vez que el navegador recibe esta transmisión, resuelve esas referencias de importación, obtiene los paquetes de componentes del cliente necesarios y monta el árbol de componentes final. Dado que los datos se envían de forma incremental, el navegador no necesita esperar a recibir todo antes de comenzar a renderizar. Esta entrega incremental es precisamente lo que permite a RSC soportar la renderización en servidor por transmisión de datos, evitando al mismo tiempo el cuello de botella habitual relacionado con la hidratación.

    Caché y revalidación

    Next.js 15 y versiones posteriores brindan a los componentes del servidor acceso a toda la gama de estrategias de caché:

    1. Renderización estática (por defecto). A menos que un componente lea datos dinámicos o realice una solicitud sin caché, se renderiza de forma estática en el momento de la compilación.
    // Cached indefinitely at build time
    async function ProductList() {
      const products = await fetch('https://api.example.com/products');
      // ...
    }
    
    1. Renderizado dinámico. Llamar a cookies(), headers() o leer searchParams, o establecer explícitamente export const dynamic = 'force-dynamic', obliga al componente a renderizarse en cada solicitud recibida.
    2. Revalidación mediante un temporizador.
    // Revalidate every 60 seconds
    async function ProductList() {
      const products = await fetch('https://api.example.com/products', {
        next: { revalidate: 60 }
      });
      // ...
    }
    
    1. Revalidación activada bajo demanda.
    // app/api/revalidate/route.ts
    import { revalidatePath } from 'next/cache';
    export async function POST() {
      revalidatePath('/products');
      return Response.json({ revalidated: true });
    }
    

    La idea clave es que el comportamiento del caché ahora está vinculado a componentes individuales en lugar de a rutas completas. Dos componentes en la misma página pueden seguir reglas de caché completamente diferentes. Esa nivelación de detalle no tiene equivalente en el SSR clásico, donde un único encabezado de caché rige toda la página a la vez.

    Malentendidos persistentes

    “Los Componentes de Servidor existen principalmente para SEO”. No del todo: una mejor indexación en búsquedas es un subproducto, no el objetivo. La verdadera motivación es reducir el JavaScript del lado del cliente y permitir que la obtención de datos se realice a nivel de componente.

    “Cualquier componente que use un hook necesita use client”. Solo es cierto cuando el hook depende realmente de las APIs del navegador. El hook use de React 19 puede desempaquetar promesas y leer el contexto directamente dentro de los Componentes de Servidor, por lo que muchos hooks funcionan bien sin necesidad de interactuar con el cliente.

    “Los Componentes de Servidor hacen obsoletas las rutas API”. No lo hacen: ambos funcionan juntos. Los Componentes de Servidor se encargan de obtener los datos para la renderización inicial, pero aún se necesitan rutas API para manejar mutaciones, webhooks provenientes de terceros y cualquier obtención de datos que ocurra en el lado del cliente después de la hidratación.

    “Los Componentes de Servidor no tienen estado”. Carecen del estado de tipo navegador: no existe useState disponible, pero pueden acceder libremente a fuentes de estado del lado del servidor como bases de datos, cachés, el sistema de archivos y variables de entorno.

    “El Contexto está prohibido en los Componentes de Servidor”. Realmente no se puede utilizar React Context dentro de un Componente de Servidor, ya que el Contexto existe específicamente para evitar la propagación de propiedades en árboles renderizados por el cliente. En su lugar, lo que se puede hacer es pasar datos como propiedades normales, y dado que el renderizado en servidor resuelve el árbol de componentes de forma síncrona de arriba hacia abajo, Next.js también ofrece opciones como unstable_rootParams además de la propagación habitual de propiedades.

    Situación actual hacia 2026

    Ahora que React 19 ha alcanzado estabilidad, las herramientas relacionadas han madurado considerablemente:

    • Las acciones del servidor permiten que un componente cliente invoque una función asíncrona que se ejecuta directamente en el servidor, suavizando la frontera entre cliente y servidor cuando se trata de mutaciones.
    • El gancho use brinda a los componentes cliente una forma de desempaquetar promesas y contexto sin necesidad de recurrir a useEffect.
    • El prerenderizado parcial (PPR) en Next.js 15 permite servir una estructura estática directamente desde el CDN, mientras que las secciones dinámicas se cargan desde el servidor de origen.
    • El compilador de React gestiona automáticamente la memorización de los componentes cliente, reduciendo las llamadas manuales a useMemo y haciendo que la transición entre el lado del cliente y el servidor sea más fluida.

    El modelo conceptual se ha estabilizado en este punto. Los Componentes del Servidor ya no son una opción que se active voluntariamente; constituyen la premisa básica sobre la cual se construyen las aplicaciones modernas de React. Los Componentes del Cliente son ahora la excepción: la vía de escape intencionada reservada para la capa interactiva.

    Un cambio en la arquitectura, no solo en el rendimiento

    Los Componentes del Servidor de React no son simplemente un ajuste para mejorar el rendimiento. Representan una reevaluación de dónde se ejecuta realmente el código de React. Durante aproximadamente diez años, React fue fundamentalmente una biblioteca para navegadores que se adaptó para ejecutarse en el lado del servidor, principalmente para satisfacer requisitos de SEO. Hoy en día es algo diferente: un sistema de componentes full-stack que opera en ambos extremos de la conexión de red, cada uno con límites, responsabilidades y perfiles de rendimiento bien definidos.

    El servidor ya no es simplemente un lugar desde donde obtener datos; ahora es un verdadero entorno de renderizado. Y el navegador ya no es el único hábitat para los componentes React; por el contrario, es precisamente allí donde reside la interactividad.

    Una vez que se comprende este enfoque, gran parte de la confusión relacionada con RSC desaparece. La pregunta pasa de “¿Necesito use client aquí?” a “¿A dónde pertenece esta lógica en particular?”. Esa es la pregunta que vale la pena hacer, y es esa mentalidad la que permite escalar a medida que las aplicaciones crecen.

    Lecturas relacionadas

  • Dejar de usar CSS en tiempo de ejecución en JS: Componentes de servidor y estilos en tiempo de compilación — Por qué el CSS en tiempo de ejecución en JS choca con los componentes de servidor de React y los objetivos de rendimiento, cómo se comparan Tailwind, CSS Modules y las bibliotecas sin tiempo de ejecución, y cómo migrar de forma segura.