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.
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:
- El navegador pide la página.
- El servidor ejecuta tus componentes, obtiene los datos y serializa el resultado a HTML.
- El navegador pinta de inmediato — ese es el LCP rápido que da SSR.
- El bundle JS se descarga y la hidratación recorre el DOM adjuntando event listeners al marcado ya existente.
- Desde ahí, la navegación del cliente funciona como una SPA normal.
SSR vs. SSG vs. CSR vs. ISR:
| Estrategia | Renderiza | Úsala para | Riesgo principal |
|---|---|---|---|
| SSR | Cada petición | Datos personalizados o en vivo | Carga de servidor, TTFB con caché fría |
| SSG | Una vez en build | Contenido estático | Contenido viejo entre despliegues |
| CSR | En el navegador | Apps con login, alta interactividad | Primer paint lento, SEO débil |
| ISR | Build + revalidación en segundo plano | Catálogos, noticias | Ventana obsoleta mientras revalida |
Variantes
| Framework | Modelo | Punto fuerte |
|---|---|---|
| Next.js | SSR + SSG + ISR + streaming | Ecosistema maduro, RSC, edge runtime |
| Nuxt | SSR + SSG + rutas híbridas | Vue, routing por archivos, motor Nitro |
| Astro | SSG-first + islas | Cero JS por defecto, islas agnósticas al framework |
| SvelteKit | SSR + CSR híbrido | Bundles pequeños, adapters listos para edge |
| Remix | SSR + mejora progresiva | Funciona 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=600permite 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.allpara llamadas independientes — los awaits secuenciales crean waterfalls que llevan el TTFB a segundos.
Errores Comunes
- Hidratar la app entera: enviar JS para contenido estático anula el propósito — audita qué componentes realmente necesitan interactividad.
- Output de render no determinista:
Date.now(),Math.random()owindowdurante el render producen HTML distinto en servidor y cliente → hydration mismatches. - 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.
- Cachear páginas personalizadas: SSR + caché
publicen una página por usuario filtra los datos de un usuario al siguiente — márcalasprivateono-store. - 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/localStorageo 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
awaitsecuencial 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 elCache-Controlde 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 metarobotsy confirma que no devuelves un shell vacío que espera datos del cliente.
Lectura Adicional
- Rendering on the Web — web.dev — el desglose de Google sobre las ventajas de SSR, SSG y CSR.
- Next.js: Server and Client Components — cómo el App Router divide el renderizado.
- Astro: Islands Architecture — la hidratación parcial explicada.
- React docs: hydrateRoot — qué hace exactamente la hidratación.
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.
Recursos Relacionados
React Server Components: Arquitectura RSC
Dominá React Server Components: arquitectura RSC, data loading, streaming, server actions y client component boundaries en Next.js App Router.
RecipeOptimización de Performance Web
Mejora Core Web Vitals, reduce tamaños de bundle y optimiza performance frontend con lazy loading, code splitting y herramientas de build modernas.
RecipeConfigurar headers HTTP Cache-Control para APIs y assets
Establece headers Cache-Control, ETag y Last-Modified para controlar la caché de navegadores y CDN en respuestas de API y assets estáticos
RecipeRendimiento SPA: Code Splitting y Lazy Loading
Mejora tiempos de carga de single-page applications dividiendo bundles a nivel de ruta y componente, implementando lazy loading con React.lazy e imports en vivo
RecipeService Worker Offline Caching para PWA en JavaScript
Cachéa assets para soporte offline PWA con Service Workers y Cache API