React Server Components vs Client-side React (SPA) Comparación

Capa de React renderizada previamente en el servidor, con cero JS por defecto

VS
Client-side React (SPA)

Forma clásica y madura de usar React, renderizada en el navegador

18 min de lecturaFrontend

Veredicto rápido

En superficies con mucho contenido y críticas para SEO, RSC aporta una ganancia medible: en el propio ejemplo de react.dev, una librería de 75K gzip nunca llega al cliente. En paneles internos muy interactivos (dashboard, admin), el enfoque client-first es más simple y presenta menos superficie de errores. La advertencia crítica es real: pasar un objeto completo como prop de Server a Client serializa todos sus campos en el flight payload; si trazas mal el límite, la ganancia se erosiona en silencio. Decide con mediciones, no con modas.

React Server ComponentsClient-side React (SPA)
Leer el veredicto completo

Comparación de puntuaciones

Cargando gráfico...

Puntuación detallada

Puntuación detallada: React Server Components y Client-side React (SPA) — puntuaciones por categoría sobre 10
CategoríaReact Server ComponentsClient-side React (SPA)
Rendimiento
8/10
6/10
Facilidad de aprendizaje
4/10
8/10
Ecosistema
7/10
9/10
Comunidad
7/10
9/10
Mercado laboral
7/10
8/10
A prueba de futuro
9/10
7/10

Pros y contras

React Server Components

Pros

  • En páginas con mucho contenido, nunca incluye librerías pesadas (parser de markdown, sanitizador) en el bundle del cliente — en el propio ejemplo de react.dev, un ahorro de 75K gzip
  • La obtención de datos en el servidor elimina el waterfall cliente-servidor (p. ej., el fetch secuencial Note→Author)
  • En la carga inicial el HTML es visible al instante; el contenido se puede leer sin esperar la hidratación de JS
  • Se integra de forma nativa con la capa ISR/caché de Next.js App Router — servido al instante con stale-while-revalidate
  • Con React 19.2 Activity y las Instant Navigations de Next.js 16.3, la pérdida de sensación de SPA se reduce en gran medida
  • La fusión de chunk-group de Turbopack optimiza automáticamente el bundle del cliente

Contras

  • Cada prop que cruza el límite Server→Client debe ser serializable; si pasas un valor no soportado, React lanza una excepción
  • Cuando 'use client' marca un archivo, TODAS sus importaciones transitivas pasan al bundle del cliente — trazar mal el límite anula la ganancia en silencio
  • Un Server Component no puede crear contexto ni usar APIs interactivas como useState/useEffect
  • Las APIs subyacentes de RSC no siguen semver entre versiones menores de React — hay que fijar el bundler/framework a una versión específica
  • El 3 de diciembre de 2025 se descubrió una vulnerabilidad crítica de RCE no autenticada (parcheada en 19.0.1/19.1.2/19.2.1) — la superficie de seguridad dependiente del framework crece
  • Curva de aprendizaje pronunciada: trazar correctamente el límite servidor/cliente requiere experiencia, y los mensajes de error suelen aparecer en tiempo de ejecución

Ideal para

Páginas con mucho contenido y críticas para SEO (blog, producto, documentación, páginas de comparación)Pantallas centradas en la carga inicial, cercanas a la fuente de datos y obtenibles de una sola vez en el servidorPáginas alimentadas por ISR/caché, que no cambian con frecuencia pero tienen mucho tráficoEquipos ya comprometidos con Next.js App Router que buscan una arquitectura nativa del frameworkProyectos que necesitan reducir de forma medible el tamaño del bundle JS del cliente

Client-side React (SPA)

Pros

  • Modelo mental simple: cada componente puede ser interactivo en todo momento, sin necesidad de pensar en el límite servidor/cliente
  • Tooling basado en Vite con dev-server rápido, HMR y code-splitting maduro (Rollup)
  • No hay límite de serialización — el estado vive directamente como objeto JS en el navegador, sin la trampa del 'flight payload'
  • El ecosistema de React más grande y antiguo; el mayor número de librerías, respuestas en Stack Overflow y talento disponible
  • No hay obligación de ejecutar un servidor — se puede desplegar en hosting estático/CDN
  • La depuración se hace en un único entorno (DevTools del navegador), sin la doble superficie de errores servidor/cliente

Contras

  • La carga inicial suele ser un `<div id="root">` vacío + el bundle JS — el TTI y el FCP convergen, y el contenido no se ve hasta que se ejecuta el JS
  • Las librerías pesadas (parser de markdown, sanitizador, librería de gráficos) siempre entran al bundle del cliente, sin opción de dejarlas en el servidor
  • Si la obtención de datos deriva en cadenas secuenciales de `useEffect`, el riesgo de waterfall cliente-servidor es alto
  • Las estrategias de un solo chunk o chunk por página pueden hacer que el código compartido se descargue una y otra vez — el problema que Turbopack intenta resolver queda, en el lado SPA, en manos del tooling (Vite/Rollup)
  • Para SEO y el rendimiento de la carga inicial hay que montar además una capa de SSG/prerender, ya que el framework en sí no la ofrece

Ideal para

Paneles internos muy interactivos (admin, dashboard) — cambios de estado frecuentes, arrastrar y soltar, filtrado en vivoAplicaciones con autenticación donde el SEO no es prioridadPrototipado rápido y desarrollo de MVP para equipos pequeñosProyectos que se desplegarán en hosting estático/CDN y no quieren el costo de ejecutar un servidorMantenimiento y modernización gradual de bases de código SPA grandes ya existentes

Comparación de código

React Server Components
// Next.js App Router — Server Component (por defecto)
// app/posts/[slug]/page.tsx
import { db } from '@/lib/db';
import LikeButton from './like-button'; // Client Component (en archivo separado con 'use client')

interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function PostPage({ params }: PageProps) {
  const { slug } = await params;

  // Await directo en el servidor — sin waterfall cliente-servidor,
  // librerías pesadas como marked/sanitize-html nunca entran al bundle del cliente
  const post = await db.post.findUnique({ where: { slug } });
  if (!post) return <div>No encontrado</div>;

  return (
    <article>
      <h1>{post.title}</h1>
      {/* Solo los props serializables pasan al Client Component */}
      <div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
      <LikeButton postId={post.id} initialCount={post.likeCount} />
    </article>
  );
}

// app/posts/[slug]/like-button.tsx
'use client';
import { useState, useTransition } from 'react';

export default function LikeButton({ postId, initialCount }: { postId: string; initialCount: number }) {
  const [count, setCount] = useState(initialCount);
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(async () => {
        setCount((c) => c + 1);
        await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
      })}
    >
      Me gusta ({count})
    </button>
  );
}
Client-side React (SPA)
// Vite + React — SPA del lado del cliente
// src/pages/PostPage.tsx
import { useEffect, useState } from 'react';
import { useParams } from 'react-router';

interface Post {
  id: string;
  title: string;
  renderedHtml: string;
  likeCount: number;
}

export default function PostPage() {
  const { slug } = useParams();
  const [post, setPost] = useState<Post | null>(null);
  const [isLiking, setIsLiking] = useState(false);

  useEffect(() => {
    // Toda la obtención de datos ocurre en el cliente — aquí empieza el riesgo de waterfall
    let cancelled = false;
    fetch(`/api/posts/${slug}`)
      .then((res) => res.json())
      .then((data) => { if (!cancelled) setPost(data); });
    return () => { cancelled = true; };
  }, [slug]);

  if (!post) return <div>Cargando...</div>; // Esta pantalla nunca aparece sin que el JS se ejecute

  async function handleLike() {
    setIsLiking(true);
    setPost((p) => (p ? { ...p, likeCount: p.likeCount + 1 } : p));
    await fetch(`/api/posts/${post!.id}/like`, { method: 'POST' });
    setIsLiking(false);
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.renderedHtml }} />
      <button disabled={isLiking} onClick={handleLike}>
        Me gusta ({post.likeCount})
      </button>
    </article>
  );
}

// vite.config.ts — el code-splitting se delega a Rollup
export default {
  build: {
    rollupOptions: {
      output: { manualChunks: { vendor: ['react', 'react-dom', 'react-router'] } },
    },
  },
};

Conclusión

En superficies con mucho contenido y críticas para SEO, RSC aporta una ganancia medible: en el propio ejemplo de react.dev, una librería de 75K gzip nunca llega al cliente. En paneles internos muy interactivos (dashboard, admin), el enfoque client-first es más simple y presenta menos superficie de errores. La advertencia crítica es real: pasar un objeto completo como prop de Server a Client serializa todos sus campos en el flight payload; si trazas mal el límite, la ganancia se erosiona en silencio. Decide con mediciones, no con modas.

Solicita una consultoría gratuita
FAQ

Preguntas frecuentes

En superficies con mucho contenido, críticas para SEO y donde importa la carga inicial (LCP/TTI) — blog, página de producto, página de comparación — RSC aporta una ganancia medible: obtención de datos en el servidor y menos JS en el cliente. Según los propios datos de Next.js 16.3, al pasar a streams nativos de Node.js en el renderizado del lado del servidor se pueden procesar un 22% más de solicitudes bajo carga. En paneles internos muy interactivos (dashboard, admin), el enfoque client-first suele ofrecer una experiencia de desarrollo más simple.

Artículos de blog relacionados

Ver todos los artículos
Todas las comparaciones