Rendering en NextJS 15
NextJS 15 introduce un cambio fundamental en cómo se construyen las aplicaciones React: React Server Components (RSC). Esta sección explica cómo funciona el rendering, cuándo se ejecuta el código en el servidor vs el cliente, y cómo aprovechar estas capacidades.
¿Qué es rendering?
Rendering es el proceso de convertir tu código React en HTML que el navegador puede mostrar. En NextJS hay dos lugares donde puede ocurrir:
- Servidor: El código se ejecuta en el servidor, genera HTML, y lo envía al navegador
- Cliente: El código se ejecuta en el navegador del usuario
Server Components vs Client Components
La innovación principal de NextJS 15 es que los componentes son Server Components por defecto:
// Este es un Server Component (por defecto)
// app/productos/page.tsx
export default async function ProductosPage() {
const productos = await obtenerProductos() // Se ejecuta en el servidor
return (
<div>
{productos.map(p => (
<div key={p.id}>{p.nombre}</div>
))}
</div>
)
}
Para crear un Client Component, usa la directiva 'use client'
:
// Este es un Client Component (explícitamente)
// components/Contador.tsx
'use client'
import { useState } from 'react'
export default function Contador() {
const [count, setCount] = useState(0) // Hooks solo en cliente
return (
<button onClick={() => setCount(count + 1)}>
Clicks: {count}
</button>
)
}
Tabla comparativa
Aspecto | Server Components | Client Components |
---|---|---|
Dónde se ejecutan | Servidor | Cliente (navegador) |
Directiva | Ninguna (por defecto) | 'use client' |
Pueden usar hooks | ❌ No | ✓ Sí |
Pueden usar event handlers | ❌ No | ✓ Sí |
Pueden hacer fetch directo | ✓ Sí | ✓ Sí (pero con limitaciones) |
Pueden acceder a DB | ✓ Sí | ❌ No (inseguro) |
Bundle de JavaScript | No se envían al cliente | Se envían al cliente |
Acceso a browser APIs | ❌ No | ✓ Sí (window, localStorage, etc.) |
Renderizado | Solo en servidor | Servidor + hidratación en cliente |
Por defecto: Server
En NextJS 15, todos los componentes son Server Components a menos que uses 'use client'
. Esto es diferente a NextJS anterior donde todo era Client Component.
Beneficios de Server Components
1. Menor JavaScript al cliente
Server Components no se envían al navegador, solo su HTML resultante:
// Server Component - NO se envía al cliente
export default async function Productos() {
const productos = await obtenerProductos() // 0 KB al cliente
const procesados = productos.map(calcularDescuento) // 0 KB al cliente
return <ProductGrid productos={procesados} />
}
Resultado: Páginas más rápidas, menos JavaScript a descargar.
2. Acceso directo a recursos del servidor
// Server Component puede acceder directo a la base de datos
import { db } from '@/lib/database'
export default async function DashboardPage() {
// Acceso directo a la DB - NO necesitas API route
const ventas = await db.ventas.findMany({
where: { fecha: new Date() }
})
return <VentasChart data={ventas} />
}
No necesitas crear API routes, el código del servidor está protegido.
3. Mejores tiempos de carga inicial
El servidor puede empezar a procesar datos antes de que el navegador descargue JavaScript:
// El servidor ya tiene los datos listos
export default async function Page() {
// Esto se ejecuta en el servidor mientras el navegador descarga JS
const [productos, categorias, destacados] = await Promise.all([
obtenerProductos(),
obtenerCategorias(),
obtenerDestacados(),
])
return <TiendaContent {...{ productos, categorias, destacados }} />
}
4. Streaming automático
NextJS puede enviar partes de la UI a medida que están listas:
import { Suspense } from 'react'
export default function Page() {
return (
<div>
{/* Se envía inmediatamente */}
<Header />
{/* Se envía cuando esté listo */}
<Suspense fallback={<Loading />}>
<ProductosLentos />
</Suspense>
</div>
)
}
Cuándo usar cada tipo
Usa Server Components cuando:
✓ Necesitas obtener datos del servidor ✓ Accedes a base de datos o APIs backend ✓ Usas secrets o API keys ✓ Tienes lógica de negocio compleja ✓ Dependes de librerías grandes (markdown, procesamiento de imágenes) ✓ El componente es principalmente presentacional
Ejemplos:
- Página de productos
- Dashboard con datos
- Blog posts
- Listas de cualquier tipo
- Componentes de layout estáticos
Usa Client Components cuando:
✓ Necesitas interactividad (onClick, onChange) ✓ Usas hooks (useState, useEffect, useRouter) ✓ Accedes a browser APIs (window, localStorage, geolocation) ✓ Necesitas event listeners ✓ Usas Context API para estado global ✓ Librerías que solo funcionan en el cliente
Ejemplos:
- Botón "Añadir al carrito"
- Formularios con validación
- Modals y dropdowns
- Carruseles de imágenes
- Barra de búsqueda con autocompletado
- Animaciones complejas
Regla de oro
Empieza con Server Components. Convierte a Client Component solo cuando necesites interactividad o hooks. Esto maximiza el rendimiento.
Estrategias de rendering
NextJS usa diferentes estrategias según cómo obtienes los datos:
Static Rendering (SSG)
Los componentes se renderizan en build time. El HTML se genera una vez y se reutiliza:
// Se genera en build time
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await obtenerPost(params.slug)
return (
<article>
<h1>{post.titulo}</h1>
<div>{post.contenido}</div>
</article>
)
}
// Genera todas las páginas en build
export async function generateStaticParams() {
const posts = await obtenerTodosPosts()
return posts.map(post => ({ slug: post.slug }))
}
Cuándo usar:
- Blog posts
- Páginas de documentación
- Landing pages
- Productos que no cambian frecuentemente
Ventajas:
- Super rápido (HTML pre-generado)
- Barato de servir (CDN)
- Excelente SEO
Dynamic Rendering (SSR)
Los componentes se renderizan en cada petición:
// Se genera en cada petición
export default async function DashboardPage() {
const ventas = await obtenerVentasHoy() // Datos en tiempo real
return <VentasChart ventas={ventas} />
}
Cuándo usar:
- Dashboards con datos en tiempo real
- Perfiles de usuario personalizados
- Carritos de compra
- Contenido que cambia frecuentemente
Ventajas:
- Siempre actualizado
- Datos específicos por usuario
- No hay fase de build
Streaming
Envía HTML parcial a medida que está listo:
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<Header /> {/* Envía inmediatamente */}
<Suspense fallback={<ProductosSkeleton />}>
<ProductosLentos /> {/* Envía cuando esté listo */}
</Suspense>
<Suspense fallback={<ResenasSkeleton />}>
<ResenasLentas /> {/* Envía cuando esté listo */}
</Suspense>
</div>
)
}
Cuándo usar:
- Páginas con secciones que tardan diferente tiempo
- Contenido importante vs secundario
- Mejorar percepción de velocidad
Ejemplos prácticos: E-commerce
Página de producto (Server + Client)
// app/productos/[slug]/page.tsx
// Server Component
export default async function ProductoPage({ params }: { params: { slug: string } }) {
// Obtener datos en el servidor
const producto = await obtenerProducto(params.slug)
return (
<div className="grid grid-cols-2 gap-8">
{/* Server Component - solo muestra datos */}
<div>
<img src={producto.imagen} alt={producto.nombre} />
<h1>{producto.nombre}</h1>
<p>{producto.descripcion}</p>
</div>
{/* Client Component - interactividad */}
<div>
<ProductoInfo
nombre={producto.nombre}
precio={producto.precio}
stock={producto.stock}
/>
</div>
</div>
)
}
// components/ProductoInfo.tsx
'use client'
import { useState } from 'react'
export default function ProductoInfo({
nombre,
precio,
stock
}: {
nombre: string
precio: number
stock: number
}) {
const [cantidad, setCantidad] = useState(1)
const handleAgregarCarrito = () => {
// Lógica de carrito (solo funciona en cliente)
agregarAlCarrito({ nombre, precio, cantidad })
}
return (
<div>
<p className="text-3xl font-bold">${precio}</p>
<div className="flex items-center gap-4">
<label>Cantidad:</label>
<input
type="number"
value={cantidad}
onChange={(e) => setCantidad(Number(e.target.value))}
max={stock}
/>
</div>
<button onClick={handleAgregarCarrito}>
Añadir al carrito
</button>
<p className="text-sm text-gray-600">
{stock} disponibles
</p>
</div>
)
}
Qué sucede:
- Server Component obtiene datos de la DB
- Genera HTML con info del producto
- Client Component se hidrata en el navegador
- Usuario puede interactuar con botones/inputs
Dashboard con streaming
// app/dashboard/page.tsx
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div className="grid grid-cols-2 gap-6">
{/* Tarjetas rápidas - se muestran inmediatamente */}
<Suspense fallback={<TarjetaSkeleton />}>
<VentasTotales />
</Suspense>
<Suspense fallback={<TarjetaSkeleton />}>
<NuevosClientes />
</Suspense>
{/* Gráficas lentas - se muestran cuando estén listas */}
<div className="col-span-2">
<Suspense fallback={<GraficaSkeleton />}>
<GraficaVentas />
</Suspense>
</div>
<div className="col-span-2">
<Suspense fallback={<TablaSkeleton />}>
<UltimasPedidos />
</Suspense>
</div>
</div>
)
}
// Cada componente obtiene sus propios datos
async function VentasTotales() {
const ventas = await calcularVentasTotales() // Query rápida
return <Tarjeta titulo="Ventas" valor={ventas} />
}
async function GraficaVentas() {
const datos = await obtenerDatosGrafica() // Query lenta
return <Grafica datos={datos} />
}
Flujo:
- Usuario ve skeletons inmediatamente
VentasTotales
se muestra primero (es rápida)GraficaVentas
reemplaza su skeleton cuando termina- Todo sucede sin bloquear la página
Carrito de compras
// app/carrito/page.tsx
// Server Component
export default async function CarritoPage() {
const userId = await obtenerUserId()
const items = await obtenerCarrito(userId)
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Tu carrito</h1>
{items.length === 0 ? (
<CarritoVacio />
) : (
<div className="grid grid-cols-3 gap-8">
{/* Lista de productos - Client para interactividad */}
<div className="col-span-2">
<ItemsCarrito items={items} />
</div>
{/* Resumen - Server Component */}
<div>
<ResumenCarrito items={items} />
<CheckoutButton />
</div>
</div>
)}
</div>
)
}
// components/ItemsCarrito.tsx
'use client'
import { useState } from 'react'
export default function ItemsCarrito({ items }: { items: Item[] }) {
const [localItems, setLocalItems] = useState(items)
const actualizarCantidad = (id: string, cantidad: number) => {
setLocalItems(items =>
items.map(item =>
item.id === id ? { ...item, cantidad } : item
)
)
// Actualizar en el servidor
actualizarCarritoServidor(id, cantidad)
}
const eliminarItem = (id: string) => {
setLocalItems(items => items.filter(item => item.id !== id))
eliminarDelCarritoServidor(id)
}
return (
<div className="space-y-4">
{localItems.map(item => (
<div key={item.id} className="flex items-center gap-4 border-b pb-4">
<img src={item.imagen} className="w-24 h-24 object-cover" />
<div className="flex-1">
<h3 className="font-semibold">{item.nombre}</h3>
<p className="text-gray-600">${item.precio}</p>
</div>
<input
type="number"
value={item.cantidad}
onChange={(e) => actualizarCantidad(item.id, Number(e.target.value))}
min="1"
className="w-16 border rounded px-2 py-1"
/>
<button
onClick={() => eliminarItem(item.id)}
className="text-red-600 hover:text-red-700"
>
Eliminar
</button>
</div>
))}
</div>
)
}
Composición de Server y Client Components
Puedes anidar componentes siguiendo estas reglas:
✓ Server puede renderizar Client
// Server Component
export default function Page() {
return (
<div>
<h1>Productos</h1>
<BotonAgregarCarrito /> {/* Client Component */}
</div>
)
}
✓ Server puede renderizar Server
// Server Component
export default function Layout({ children }) {
return (
<div>
<Header /> {/* Server Component */}
{children}
<Footer /> {/* Server Component */}
</div>
)
}
✓ Client puede renderizar Client
'use client'
export default function Modal() {
return (
<div>
<ModalHeader /> {/* Client Component */}
<ModalContent /> {/* Client Component */}
</div>
)
}
⚠️ Client NO puede importar Server directamente
'use client'
// ❌ Esto NO funciona
import ServerComponent from './ServerComponent'
export default function ClientComponent() {
return <ServerComponent /> // Error
}
Solución: Pasa el Server Component como children:
// Server Component padre
export default function Page() {
return (
<ClientComponent>
<ServerComponent /> {/* Pasa como children */}
</ClientComponent>
)
}
// Client Component recibe children
'use client'
export default function ClientComponent({ children }) {
return (
<div>
<button>Botón interactivo</button>
{children} {/* Renderiza el Server Component */}
</div>
)
}
Patrón children
Este patrón es fundamental para combinar Server y Client Components. El componente servidor se pasa como prop children
al componente cliente.
Detección automática de rendering
NextJS decide automáticamente cómo renderizar según tu código:
Static (por defecto)
// No usa funciones dinámicas → Static
export default async function Page() {
const posts = await fetch('https://api.ejemplo.com/posts')
return <div>{/* ... */}</div>
}
Dynamic (automático)
NextJS cambia a dynamic cuando usas:
// Usa cookies → Dynamic automático
import { cookies } from 'next/headers'
export default async function Page() {
const token = cookies().get('token')
return <div>{/* ... */}</div>
}
Funciones que activan dynamic:
cookies()
headers()
searchParams
en páginasfetch()
concache: 'no-store'
revalidate: 0
en fetch
Forzar dynamic
// Forzar dynamic rendering
export const dynamic = 'force-dynamic'
export default async function Page() {
// Siempre se renderiza en cada petición
return <div>{/* ... */}</div>
}
Mejores prácticas
1. Server Components por defecto
// ✓ Bueno - Server por defecto
export default async function Page() {
const data = await obtenerDatos()
return <Lista data={data} />
}
// Solo client cuando sea necesario
'use client'
export function BotonInteractivo() {
const [active, setActive] = useState(false)
return <button onClick={() => setActive(!active)} />
}
2. Mueve 'use client' lo más abajo posible
// ❌ Malo - todo el árbol es client
'use client'
export default function Page() {
return (
<div>
<Header />
<ProductList />
<BotonInteractivo /> {/* Solo esto necesita ser client */}
</div>
)
}
// ✓ Bueno - solo el botón es client
export default function Page() {
return (
<div>
<Header /> {/* Server */}
<ProductList /> {/* Server */}
<BotonInteractivo /> {/* Client */}
</div>
)
}
3. Usa Suspense para streaming
// ✓ Bueno - streaming de partes independientes
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<Header />
<Suspense fallback={<Skeleton />}>
<ComponenteLento />
</Suspense>
<Footer />
</div>
)
}
4. Server Components para data fetching
// ✓ Bueno - fetch en Server Component
export default async function ProductosPage() {
const productos = await fetch('https://api.ejemplo.com/productos')
return <ProductGrid productos={productos} />
}
// ❌ Evita - fetch en Client Component
'use client'
export default function ProductosPage() {
const [productos, setProductos] = useState([])
useEffect(() => {
fetch('https://api.ejemplo.com/productos')
.then(res => res.json())
.then(setProductos)
}, [])
return <ProductGrid productos={productos} />
}
5. Mantén Client Components pequeños
// ✓ Bueno - Client Component pequeño y enfocado
'use client'
export function AddToCartButton({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false)
const handleAdd = async () => {
setLoading(true)
await agregarAlCarrito(productId)
setLoading(false)
}
return (
<button onClick={handleAdd} disabled={loading}>
{loading ? 'Agregando...' : 'Agregar al carrito'}
</button>
)
}
Debugging del rendering
Ver qué es Server vs Client
Usa console.log
para identificar:
// Server Component
export default async function Page() {
console.log('Esto se imprime en la TERMINAL del servidor')
return <div>Página</div>
}
// Client Component
'use client'
export default function Button() {
console.log('Esto se imprime en la CONSOLA del navegador')
return <button>Click</button>
}
Verificar bundle size
Los Server Components no aumentan el bundle:
npm run build
Verás el tamaño de cada ruta. Server Components no agregan JavaScript al bundle.
Resumen
Conceptos clave de Rendering:
- Los componentes son Server Components por defecto en NextJS 15
- Usa
'use client'
solo cuando necesites interactividad o hooks - Server Components son mejores para data fetching y lógica de servidor
- Client Components son necesarios para interactividad y browser APIs
- NextJS elige automáticamente entre Static y Dynamic rendering
- Usa Suspense para streaming y mejor percepción de velocidad
- Mueve
'use client'
lo más abajo posible en el árbol de componentes - Server Components pueden renderizar Client Components como children
- Menos JavaScript = páginas más rápidas
- Aprovecha lo mejor de ambos mundos combinándolos apropiadamente
Tabla de decisión rápida:
Necesitas | Usa |
---|---|
Obtener datos de DB | Server Component |
onClick, onChange | Client Component |
useState, useEffect | Client Component |
Acceso a secrets/API keys | Server Component |
window, localStorage | Client Component |
Componente puramente visual | Server Component |
Context API | Client Component |
Reducir bundle de JS | Server Component |