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:

  1. Servidor: El código se ejecuta en el servidor, genera HTML, y lo envía al navegador
  2. 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

AspectoServer ComponentsClient Components
Dónde se ejecutanServidorCliente (navegador)
DirectivaNinguna (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 JavaScriptNo se envían al clienteSe envían al cliente
Acceso a browser APIs❌ No✓ Sí (window, localStorage, etc.)
RenderizadoSolo en servidorServidor + 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:

  1. Server Component obtiene datos de la DB
  2. Genera HTML con info del producto
  3. Client Component se hidrata en el navegador
  4. 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:

  1. Usuario ve skeletons inmediatamente
  2. VentasTotales se muestra primero (es rápida)
  3. GraficaVentas reemplaza su skeleton cuando termina
  4. 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áginas
  • fetch() con cache: '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:

  1. Los componentes son Server Components por defecto en NextJS 15
  2. Usa 'use client' solo cuando necesites interactividad o hooks
  3. Server Components son mejores para data fetching y lógica de servidor
  4. Client Components son necesarios para interactividad y browser APIs
  5. NextJS elige automáticamente entre Static y Dynamic rendering
  6. Usa Suspense para streaming y mejor percepción de velocidad
  7. Mueve 'use client' lo más abajo posible en el árbol de componentes
  8. Server Components pueden renderizar Client Components como children
  9. Menos JavaScript = páginas más rápidas
  10. Aprovecha lo mejor de ambos mundos combinándolos apropiadamente

Tabla de decisión rápida:

NecesitasUsa
Obtener datos de DBServer Component
onClick, onChangeClient Component
useState, useEffectClient Component
Acceso a secrets/API keysServer Component
window, localStorageClient Component
Componente puramente visualServer Component
Context APIClient Component
Reducir bundle de JSServer Component