Durante una década, el frontend se movió inexorablemente hacia el cliente. SPAs, client-side rendering, hidratación masiva. Ahora, React Server Components y Next.js App Router están invirtiendo la tendencia. El servidor ha vuelto, y esta vez es personal.
La Era del Client-Side Rendering (y sus Problemas)
Alrededor de 2015, la industria adoptó masivamente las Single Page Applications. La promesa era clara: experiencias de usuario fluidas, sin recargas de página, con toda la lógica en el navegador. Frameworks como React, Angular y Vue dominaron.
Pero este modelo tenía costos ocultos que se hicieron evidentes con el tiempo:
- Bundles JavaScript gigantes: Aplicaciones que enviaban 2-5 MB de JS al cliente.
- Time to Interactive (TTI) degradado: El usuario veía una página en blanco mientras se descargaba, parseaba y ejecutaba el JS.
- SEO problemático: Los crawlers no siempre ejecutaban JavaScript correctamente.
- Waterfalls de red: El JS se descargaba, luego hacía fetch de datos, luego renderizaba. Tres viajes de red secuenciales.
React Server Components: Un Nuevo Paradigma
Los React Server Components (RSC) no son simplemente "renderizado en el servidor". Son un modelo mental completamente nuevo donde los componentes se clasifican en dos categorías:
Server Components (por defecto)
Se ejecutan solo en el servidor. Nunca envían JavaScript al cliente. Pueden acceder directamente a bases de datos, sistemas de archivos y APIs internas sin exponer credenciales.
// app/users/page.js — Server Component (por defecto)
import { db } from '@/lib/database';
export default async function UsersPage() {
// Esto se ejecuta en el servidor. Sin "use client", sin fetch().
const users = await db.query('SELECT * FROM users LIMIT 100');
return (
<main>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</main>
);
}
Client Components (opt-in)
Se marcan explícitamente con "use client". Se hidratan en el navegador y pueden usar hooks como useState, useEffect, y event handlers.
// components/LikeButton.jsx — Client Component
"use client";
import { useState } from 'react';
export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes(likes + 1)}>
❤️ {likes}
</button>
);
}
La Composición: Server + Client
El poder real de RSC emerge cuando compones ambos tipos. Un Server Component puede renderizar un Client Component, pasándole datos ya fetched como props:
// app/posts/[id]/page.js — Server Component
import { db } from '@/lib/database';
import LikeButton from '@/components/LikeButton';
import CommentSection from '@/components/CommentSection';
export default async function PostPage({ params }) {
const post = await db.post.findUnique({ where: { id: params.id } });
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
{/* Client Component con datos del servidor */}
<LikeButton initialLikes={post.likes} />
{/* Otro Client Component */}
<CommentSection postId={post.id} />
</article>
);
}
"Los Server Components no reemplazan a los Client Components. Los complementan. Es como tener dos herramientas especializadas en lugar de una herramienta genérica."
Streaming y Suspense: La Magia del Renderizado Progresivo
Con RSC, el servidor puede transmitir el HTML progresivamente. No tienes que esperar a que toda la página esté lista; las partes que dependen de datos lentos pueden llegar después.
// app/dashboard/page.js
import { Suspense } from 'react';
import UserStats from '@/components/UserStats';
import RecentActivity from '@/components/RecentActivity';
import Recommendations from '@/components/Recommendations';
export default function Dashboard() {
return (
<div className="grid grid-cols-3 gap-6">
{/* Se renderiza inmediatamente */}
<h1>Dashboard</h1>
{/* Cada sección se carga independientemente */}
<Suspense fallback={<Skeleton />}>
<UserStats /> {/* Consulta rápida: ~50ms */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<RecentActivity /> {/* Consulta media: ~200ms */}
</Suspense>
<Suspense fallback={<Skeleton />}>
<Recommendations /> {/* ML inference: ~500ms */}
</Suspense>
</div>
);
}
El usuario ve el header y los skeletons instantáneamente. Luego, cada sección "aparece" cuando sus datos están listos, sin bloquear las demás.
Server Actions: Mutaciones sin API Routes
Quizás la feature más revolucionaria de Next.js 14+ son los Server Actions. Permiten definir funciones que se ejecutan en el servidor y se invocan directamente desde formularios o event handlers, sin crear API routes manualmente.
// app/contact/page.js
async function submitForm(formData) {
"use server"; // Esta función se ejecuta en el servidor
const email = formData.get('email');
const message = formData.get('message');
await db.contact.create({
data: { email, message }
});
// Revalidar la caché si es necesario
revalidatePath('/contact');
}
export default function ContactPage() {
return (
<form action={submitForm}>
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Enviar</button>
</form>
);
}
Esto funciona sin JavaScript en el cliente. El formulario hace un POST tradicional. Pero si JS está disponible, Next.js lo intercepta y hace la mutación con fetch, proporcionando una experiencia más fluida.
Caching y Revalidación
Next.js App Router introduce un sistema de caché de múltiples capas que puede ser confuso al principio, pero es increíblemente potente:
- Request Memoization: Deduplicación automática de fetches idénticos en un mismo render.
- Data Cache: Persistencia de resultados de fetch entre requests.
- Full Route Cache: HTML pre-renderizado para rutas estáticas.
- Router Cache: Caché client-side de segmentos de ruta visitados.
// Revalidación basada en tiempo
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // Revalidar cada hora
});
// Revalidación on-demand
import { revalidateTag } from 'next/cache';
async function updateProduct(id) {
"use server";
await db.product.update({ where: { id }, data: {...} });
revalidateTag('products'); // Invalida todas las fetches con este tag
}
RSC no es "todo servidor" ni "todo cliente". Es elegir la herramienta correcta para cada componente. Datos estáticos en el servidor, interactividad en el cliente. Simple, pero profundo.