StackPractices
intermediate Por Mathias Paulenko

Renderizado en el servidor (SSR)

Mejora el rendimiento y el SEO con renderizado en el servidor usando Next.js, Nuxt, Astro y otros frameworks con estrategias de hidratación.

Temas: frontend

Resumen

El renderizado en el servidor (SSR) genera el HTML de cada petición en el servidor y envía una página completamente renderizada al navegador. El usuario ve contenido real de inmediato, los buscadores indexan el marcado completo (crítico para el SEO) y las vistas previas sociales obtienen las etiquetas Open Graph correctas. Los frameworks modernos — Next.js, Nuxt, Astro, SvelteKit — envían después JavaScript que “hidrata” ese HTML estático y lo convierte en una app interactiva.

El SSR no es gratis: pagas un render por cada petición y cachear se complica respecto a una exportación estática. Esta receta cubre cuándo vale la pena ese coste, cómo implementarlo en los frameworks principales y cómo evitar los problemas de producción que sufren casi todos los despliegues con SSR.

Cuándo Usarlo

  • Páginas con mucho contenido que dependen de la indexación en buscadores (docs, catálogos, artículos).
  • Páginas compartidas en redes sociales donde las vistas previas OG tienen que ser correctas.
  • Landing pages públicas donde un primer paint lento daña los Core Web Vitals y la conversión.
  • Páginas personalizadas donde el SSG no puede generar el contenido correcto en build time.

Cuándo NO Usarlo

  • Dashboards internos detrás de autenticación — los crawlers nunca los ven, así que CSR sale más barato.
  • Páginas que dependen de estado vivo del cliente (apps offline-first, canvas, editores).
  • Páginas con muchísimo tráfico y contenido casi estático — SSG detrás de un CDN las resuelve a una fracción del coste de servidor.

Solución

Next.js App Router con Streaming SSR

// app/products/page.tsx — se renderiza en el servidor en cada petición
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }, // ISR: sirve desde caché 60s y revalida en segundo plano
  });
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();

  return (
    <main>
      <h1>Products</h1>
      <ul>
        {products.map((p) => (
          <li key={p.id}>
            {p.name} — ${p.price}
          </li>
        ))}
      </ul>
    </main>
  );
}

Los server components asíncronos obtienen los datos en el servidor y ese código nunca llega al navegador — los secretos quedan a salvo y el bundle del cliente se mantiene pequeño. Para SSR puro sin caché, pasa cache: 'no-store' al fetch.

Astro con arquitectura de islas

---
// Corre en el servidor (o en build time para páginas estáticas)
const response = await fetch('https://api.example.com/stats');
const stats = await response.json();
---

<html>
  <body>
    <h1>Dashboard</h1>
    <!-- HTML plano, cero JS enviado -->
    <p>Total Users: {stats.users}</p>

    <!-- Solo esta isla se hidrata en el cliente -->
    <LiveChart client:load data={stats.chart} />
  </body>
</html>

Astro renderiza todo a HTML por defecto y solo hidrata los componentes que marques con una directiva client:* — hidratación parcial en lugar de hidratar el árbol entero.

Nuxt 3 SSR con useFetch

<script setup>
const { data: posts } = await useFetch('/api/posts', {
  server: true, // obtiene los datos en el servidor durante SSR
  default: () => [],
});
</script>

<template>
  <div>
    <h1>Blog</h1>
    <article v-for="post in posts" :key="post.id">
      <h2>{{ post.title }}</h2>
      <p>{{ post.excerpt }}</p>
    </article>
  </div>
</template>

SSR mínimo a mano (Express + React)

Útil para entender qué abstraen los frameworks. No lo uses en producción — no incluye streaming, code splitting ni manejo de assets.

// server.js — renderiza el árbol de React a HTML por petición
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { App } from './App.js';

const app = express();

app.get('*', (req, res) => {
  const html = renderToString(<App url={req.url} />);
  res.send(`<!doctype html>
    <html><body>
      <div id="root">${html}</div>
      <script src="/client.js"></script>  <!-- hidrata el mismo árbol -->
    </body></html>`);
});

app.listen(3000);

Cómo Funciona

El ciclo de vida de la petición:

flowchart diagram: Petición del navegador
  1. El navegador pide la página.
  2. El servidor ejecuta tus componentes, obtiene los datos y serializa el resultado a HTML.
  3. El navegador pinta de inmediato — ese es el LCP rápido que da SSR.
  4. El bundle JS se descarga y la hidratación recorre el DOM adjuntando event listeners al marcado ya existente.
  5. Desde ahí, la navegación del cliente funciona como una SPA normal.

SSR vs. SSG vs. CSR vs. ISR:

EstrategiaRenderizaÚsala paraRiesgo principal
SSRCada peticiónDatos personalizados o en vivoCarga de servidor, TTFB con caché fría
SSGUna vez en buildContenido estáticoContenido viejo entre despliegues
CSREn el navegadorApps con login, alta interactividadPrimer paint lento, SEO débil
ISRBuild + revalidación en segundo planoCatálogos, noticiasVentana obsoleta mientras revalida

Variantes

FrameworkModeloPunto fuerte
Next.jsSSR + SSG + ISR + streamingEcosistema maduro, RSC, edge runtime
NuxtSSR + SSG + rutas híbridasVue, routing por archivos, motor Nitro
AstroSSG-first + islasCero JS por defecto, islas agnósticas al framework
SvelteKitSSR + CSR híbridoBundles pequeños, adapters listos para edge
RemixSSR + mejora progresivaFunciona sin JS; carga de datos anidada

Buenas Prácticas

  • Streamea los boundaries lentos: envuelve componentes lentos en <Suspense> para que el primer chunk de HTML llegue rápido y el resto llegue por streaming.
  • Hidrata lo mínimo posible: usa islas (client:visible, dynamic imports) en lugar de hidratar el árbol completo.
  • Serializa el estado mínimo: pasa al cliente solo lo que necesita — nunca un volcado crudo de la base de datos.
  • Cachea la salida SSR en el edge: Cache-Control: public, s-maxage=300, stale-while-revalidate=600 permite que un CDN absorba la mayoría de peticiones — mira la referencia completa en headers HTTP Cache-Control.
  • Mantén los secretos en el servidor: los server components y loaders pueden leer variables de entorno; todo lo que importe un client component viaja al navegador.
  • Paraleliza los fetch del servidor: Promise.all para llamadas independientes — los awaits secuenciales crean waterfalls que llevan el TTFB a segundos.

Errores Comunes

  1. Hidratar la app entera: enviar JS para contenido estático anula el propósito — audita qué componentes realmente necesitan interactividad.
  2. Output de render no determinista: Date.now(), Math.random() o window durante el render producen HTML distinto en servidor y cliente → hydration mismatches.
  3. Bloquear la página por una query lenta: una sola llamada API de 5 segundos retrasa cada byte; aíslala detrás de un boundary o streámela.
  4. Cachear páginas personalizadas: SSR + caché public en una página por usuario filtra los datos de un usuario al siguiente — márcalas private o no-store.
  5. Sin fallback ante errores: si SSR falla a mitad de petición, devuelve un shell estático degradado en lugar de un 500 crudo.

Solución de Problemas

  • Warnings de hydration mismatch: compara el HTML del servidor con el primer render del cliente — los culpables habituales son timestamps, aleatoriedad, acceso a window/localStorage o extensiones del navegador que inyectan marcado antes de la hidratación.
  • La página renderiza y luego parpadea con contenido distinto: misma causa — React descartó el DOM del servidor y re-renderizó; arregla el no-determinismo, no suprimas el warning.
  • TTFB alto en una ruta que antes era rápida: busca una nueva cadena de await secuencial o una llamada API sin caché en el render; revisa los logs del servidor por fetches en waterfall.
  • Los usuarios ven datos de otros usuarios: una caché compartida (CDN o caché de fetch) trata una respuesta personalizada como pública — inspecciona el Cache-Control de la respuesta y la cache key (Vary: Cookie/Authorization).
  • La memoria crece hasta tumbar el servidor: cachés a nivel de módulo, event listeners o intervals registrados durante el render viven para siempre — acota el trabajo por petición dentro del ciclo de vida de la petición.
  • Los crawlers siguen sin indexar la página: verifica que la salida SSR contiene de verdad el contenido con curl -L (sin JS); revisa el meta robots y confirma que no devuelves un shell vacío que espera datos del cliente.

Lectura Adicional

Las versiones ejecutables de estos ejemplos están en el repositorio companion: stack-practices-resources/resources/recipes/frontend/server-side-rendering.

Preguntas frecuentes

¿Cuál es la diferencia entre SSR e hidratación?

SSR produce el HTML en el servidor; la hidratación es el paso del cliente donde JavaScript adjunta los event handlers a ese HTML. SSR te da el primer paint rápido; la hidratación te da interactividad. Necesitas ambas para que el modelo completo funcione.

¿El SSR perjudica el rendimiento?

Mejora el primer paint pero añade coste de servidor por petición y puede hacer el TTFB más lento que un archivo estático. Se mitiga con streaming, caché en el edge e ISR. Para contenido que no cambia por usuario, SSG es casi siempre más rápido y más barato.

¿Uso SSR o SSG para mi blog?

SSG. El contenido de un blog cambia al desplegar, no por petición, así que construir una vez y servir desde un CDN gana en todas las métricas. Recurre a SSR solo cuando el output depende de la petición — sesión, geolocalización, stock en vivo.

¿Cómo arreglo los errores de hydration mismatch?

Quita el código no determinista del render: mueve Date.now(), Math.random() y las APIs del navegador dentro de useEffect/onMount o de una directiva client:*, para que servidor y cliente produzcan el mismo HTML inicial. En desarrollo, React señala el subárbol que difiere.

¿Puedo mezclar SSR y SSG en la misma app?

Sí — es la configuración habitual en producción. Las páginas estáticas de marketing y docs van a SSG; las rutas autenticadas o personalizadas van a SSR. Next.js, Nuxt y Astro permiten elegir por ruta.

¿El SSR mejora el SEO?

Para páginas que rastrea Google, renderizar en el servidor elimina la dependencia de la segunda ola de renderizado de Google, que ejecuta el JS más tarde y con menos fiabilidad. El contenido en SSR se indexa antes y de forma más consistente — no salva contenido pobre, pero quita un riesgo técnico.

¿Qué es el streaming SSR?

En lugar de esperar a todos los datos antes de enviar nada, el servidor envía el HTML por chunks: primero el shell y luego cada sección resuelta dentro de los boundaries <Suspense>. El usuario ve el esqueleto de la página en milisegundos aunque un componente lento tarde segundos.

¿El SSR puede exponer secretos al cliente?

Solo si los colocas ahí. Las variables de entorno leídas dentro de un server component o loader se quedan en el servidor; todo lo que referencie un client component se empaqueta en el JS enviado. Audita los client components por importaciones accidentales de módulos solo para servidor.