Server Components en Next.js

Qué son los React Server Components, por qué Next.js los usa por defecto, cómo funcionan internamente y cuándo usarlos frente a los Client Components.

Server Components en Next.js

Serie: Entendiendo Next.js — Este artículo es parte de la serie "Entendiendo Next.js", donde exploramos cómo funciona Next.js internamente.

React siempre corrió en el browser. Esa era la premisa. Los Server Components la cambiaron — y entender por qué React tomó esa decisión es clave para usarlos bien.

El problema que venían a resolver

En una app React tradicional, el flujo es siempre el mismo:

Browser descarga JS  →  React ejecuta el código  →  DOM aparece

Ese modelo tiene tres problemas concretos que se vuelven visibles cuando las apps crecen:

  • Waterfalls de datos — el componente monta, dispara un useEffect, espera la respuesta, actualiza el estado, re-renderiza. Si hay componentes anidados que también piden datos, cada uno espera al anterior.
  • Bundle size — toda la lógica de negocio, las librerías de formateo, los helpers de datos: todo viaja al browser aunque el usuario nunca la necesite directamente.
  • Secrets expuestos — para hacer fetch a una API privada desde el cliente, necesitás un intermediario (una API route) o exponés credenciales.

React publicó el RFC de Server Components en 2020. La propuesta era simple: algunos componentes pueden correr exclusivamente en el servidor. Nunca llegan al browser. Nunca agregan peso al bundle. Pueden tocar la base de datos directamente.

Next.js App Router (Next.js 13+) fue el primer framework en integrarlos de forma estable.

Qué son

Un Server Component es un componente de React que se ejecuta solo en el servidor. El servidor lo renderiza, genera el HTML y el RSC Payload, y los envía al browser.

El browser nunca recibe el código fuente del componente. Recibe el resultado — el HTML que ese componente produjo.

tsx
// app/posts/page.tsx
// Este archivo nunca llega al browser

interface Post {
  id: number
  title: string
  slug: string
}

async function getPosts(): Promise<Post[]> {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()  // await directo, sin useEffect

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>
          <a href={`/posts/${post.slug}`}>{post.title}</a>
        </li>
      ))}
    </ul>
  )
}

Nada de esto llega al browser: ni getPosts, ni la URL de la API, ni la lógica del componente. Solo el HTML renderizado.

Por qué son el default en App Router

En app/, todo componente es Server Component por defecto. No necesitás hacer nada para activarlos — simplemente no agregás 'use client'.

Esta fue una decisión deliberada del equipo de Next.js: el caso más común (mostrar datos) no debería requerir JavaScript en el cliente. La interactividad es la excepción, no la regla.

Si querés un Client Component, lo declarás explícitamente con 'use client'. Si no hacés nada, es Server Component.

Qué código llega al browser

Esta es la diferencia más importante de entender, y vale verla con un ejemplo concreto.

Supongamos que tenés una página de artículo que usa date-fns para formatear fechas y hace fetch a una API:

tsx
// app/posts/[id]/page.tsx — Server Component
import { format } from 'date-fns'  // librería de 13kb
import { es } from 'date-fns/locale'

interface Post {
  id: number
  title: string
  content: string
  publishedAt: string
}

async function getPost(id: string): Promise<Post> {
  // API key nunca sale del servidor
  const res = await fetch(`https://cms.example.com/posts/${id}`, {
    headers: { Authorization: `Bearer ${process.env.CMS_API_KEY}` },
  })
  return res.json()
}

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)

  // format() corre en el servidor — date-fns no va al bundle del browser
  const fecha = format(new Date(post.publishedAt), "d 'de' MMMM, yyyy", {
    locale: es,
  })

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{fecha}</time>
      <p>{post.content}</p>
    </article>
  )
}

Lo que llega al browser:

html
<article>
  <h1>Cómo funciona el rendering en Next.js</h1>
  <time>13 de marzo, 2026</time>
  <p>Next.js no es solo React con routing...</p>
</article>

date-fns (13kb), getPost, process.env.CMS_API_KEY — nada de eso llega al cliente. Cero kilobytes de JS para este componente.

Cómo funcionan internamente

Los Server Components son componentes de React normales con dos diferencias fundamentales:

Pueden ser async, lo que permite hacer await directamente en el cuerpo del componente, sin useEffect ni estado intermedio.

No tienen ciclo de vida en el cliente, porque nunca se montan ahí. No existe useState, useEffect, useRef, ni nada que dependa del browser.

El proceso de ejecución es:

Request llega al servidor
         │
         ▼
Next.js ejecuta el componente (async, await, fetch, DB)
         │
         ▼
Genera HTML + RSC Payload
(RSC Payload = representación del árbol de componentes + props)
         │
         ▼
Browser recibe HTML → muestra contenido inmediatamente
Browser recibe RSC Payload → React reconcilia el árbol
         │
         ▼
Solo los Client Components se hidratan

Acceso directo a la base de datos

Una de las ventajas más concretas: los Server Components pueden tocar la base de datos directamente, sin necesidad de crear una API route intermedia.

tsx
// app/dashboard/page.tsx
import { db } from '@/lib/db'  // cliente de Prisma, Drizzle, o similar

export default async function DashboardPage() {
  // Query directo — esto corre en el servidor
  const orders = await db.order.findMany({
    where: { status: 'pending' },
    orderBy: { createdAt: 'desc' },
    take: 10,
  })

  return (
    <section>
      <h2>Órdenes pendientes</h2>
      <ul>
        {orders.map(order => (
          <li key={order.id}>
            #{order.id}{order.total}
          </li>
        ))}
      </ul>
    </section>
  )
}

No hace falta fetch('/api/orders'). No hace falta una API route. El query corre en el servidor, el resultado llega al browser como HTML.

Composición con Client Components

Un Server Component puede renderizar Client Components. Un Client Component puede recibir Server Components como children. Esa combinación es la base de cómo se construye una app en Next.js.

El patrón más común: el layout o la página es Server Component, los elementos interactivos son Client Components que reciben datos como props.

tsx
// app/products/[id]/page.tsx — Server Component
import { AddToCartButton } from '@/components/add-to-cart-button'
import { db } from '@/lib/db'

export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({
    where: { id: params.id },
  })

  if (!product) return <p>Producto no encontrado</p>

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <p className="text-2xl font-bold">${product.price}</p>

      {/* Client Component recibe solo los datos que necesita */}
      <AddToCartButton productId={product.id} stock={product.stock} />
    </article>
  )
}
tsx
// components/add-to-cart-button.tsx — Client Component
'use client'

import { useState } from 'react'

interface Props {
  productId: string
  stock: number
}

export function AddToCartButton({ productId, stock }: Props) {
  const [added, setAdded] = useState(false)

  if (stock === 0) return <p>Sin stock</p>

  return (
    <button
      onClick={() => setAdded(true)}
      disabled={added}
    >
      {added ? 'Agregado al carrito ✓' : 'Agregar al carrito'}
    </button>
  )
}

El Server Component hizo el trabajo pesado (query a la DB, lógica de negocio). El Client Component recibe solo los datos serializables que necesita para la interactividad.

Proteger código del servidor

Cuando tenés funciones con lógica sensible — queries a la DB, llamadas con API keys, acceso a variables de entorno privadas — podés asegurarte de que nunca se importen en un Client Component usando el paquete server-only:

bash
npm install server-only
ts
// lib/data.ts
import 'server-only'  // error en build si se importa en cliente

export async function getUserData(userId: string) {
  return db.user.findUnique({
    where: { id: userId },
    select: { name: true, email: true, plan: true },
  })
}

Si alguien intenta importar getUserData en un componente con 'use client', Next.js lanza un error en build time. No en runtime — en build. No puede llegar a producción.

Cuándo usarlos

La regla es simple: si el componente no necesita interactividad, usá Server Component. Es el default — no hacés nada.

Casos ideales:

  • Fetch de datos — lista de posts, detalle de producto, dashboard con métricas
  • Acceso a la DB — queries de Prisma, Drizzle, o cualquier ORM
  • Lógica de negocio — formateo, filtros, cálculos que no necesitan input del usuario
  • Componentes que usan librerías pesadas — parsers de markdown, formateo de fechas, generación de gráficos estáticos
  • Layout y estructura — headers, footers, sidebars que no cambian según el usuario

Cuándo NO usarlos

Hay situaciones donde necesitás un Client Component sí o sí:

  • EstadouseState, useReducer
  • EfectosuseEffect, useLayoutEffect
  • EventosonClick, onChange, onSubmit
  • APIs del browserlocalStorage, sessionStorage, window, navigator
  • Hooks de librerías de UIuseRouter de Next.js, hooks de animación, drag and drop

Si intentás usar cualquiera de estas cosas en un Server Component, vas a ver un error. No es un warning — es un error que impide que el código funcione.

Comparativa rápida

Server ComponentClient Component
DirectivaNinguna (default)'use client'
Corre enSolo servidorServidor + browser
JS al browserNinguno
useState, useEffectNo
onClick, eventosNo
await directoNo
Acceso a DBNo (sin API route)
Secrets segurosNo
Cuándo usarloDatos, lógica, estructuraInteractividad, estado, browser APIs

La regla práctica

Empezá siempre con Server Component. Si en algún punto necesitás estado, eventos o APIs del browser, extraé esa parte a un componente separado y agregale 'use client'. Hacelo lo más pequeño posible.

El resultado: la mayor parte de tu app es Server Component, llega como HTML, no agrega JS al bundle. Solo los elementos interactivos son Client Components, y están acotados a lo mínimo necesario.

Emanuel López

Emanuel López

Desarrollador de Software · Montevideo, Uruguay

Mantente al día

Te aviso cuando publique algo nuevo. Podés darte de baja cuando quieras.