Lo que aprendí del SSR en Vue (y por qué JSP ya era SSR)
Notas a partir de la charla Things I Learned from SSR (「從入門到被開除」 — del onboarding al despido, N cosas que aprendí del SSR) combinadas con una conversación propia sobre CSR vs SSR en Vue.
La base en dos párrafos
CSR (el flujo clásico de Vue 3 + Vite): el servidor manda un HTML casi vacío más el bundle de JS, y el navegador construye el DOM. SSR (típicamente Nuxt 3): el servidor ejecuta Vue, genera el HTML completo, y el navegador lo muestra al instante; luego Vue “adopta” ese DOM y le añade los event listeners — la hydration.
De la hydration sale la regla de oro del SSR: el código de servidor y cliente debe producir el mismo resultado. Date.now(), Math.random() o cualquier acceso a window son fuentes clásicas de hydration mismatch — uno de los bugs más frustrantes de debuggear.
Los bugs que solo existen en SSR
La parte más valiosa de la charla son los problemas que no existen en CSR, y todos comparten la misma causa raíz: el servidor vive indefinidamente; el navegador no.
Memory leaks y request pollution. En el browser cada navegación destruye el estado. En el servidor, la misma instancia de Node atiende miles de requests — un store definido como singleton global se comparte entre todos los usuarios: el estado se acumula o, peor, se filtra entre requests concurrentes. La solución es siempre el patrón factory: una instancia nueva por request.
// ❌ singleton global — compartido entre todos los requests
const store = { user: null }
// ✅ factory — instancia nueva por request
export function createStore() {
return { user: null }
}
URLs relativas. En el browser fetch('/api/data') funciona porque existe window.location. En Node no hay origen implícito — axios.get('/api/posts') lanza Invalid URL. O configuras baseURL, o usas $fetch de Nuxt que lo resuelve internamente en ambos contextos.
El waterfall de awaits. Dos await secuenciales en <script setup> suman sus latencias (300ms + 200ms = 500ms); Promise.all las solapa (300ms). Y el combo useAsyncData + watch puede disparar un fetch duplicado al montar: Suspense resuelve el primero, y el watch — que acaba de montarse — lanza el segundo.
El paralelo que me interesa: JSP ya era esto
Trabajando con Spring Boot + JSP en unos proyectos y Vue en otros, este cuadro me ordenó las ideas:
| JSP + jQuery | Nuxt SSR | |
|---|---|---|
| ¿Quién decide el orden de datos? | Tú — imperativo | El framework — declarativo |
| ¿Bloquea el render? | Nunca — el HTML ya está | Sí, hasta que el async setup resuelve |
| Render parcial | Gratis | Requiere Suspense anidados |
JSP se comporta más parecido a Nuxt SSR que a Vue CSR: el HTML llega renderizado del servidor y el JS es un añadido posterior — progressive enhancement, aunque en 2005 nadie lo llamara así. Quien viene de JSP + Spring Boot ya tiene la intuición del SSR; lo que cambia es quién orquesta (el framework en vez de ti) y el precio nuevo de la hydration.
Cuándo usar cada uno
Mi resumen operativo: app interna, dashboard o admin → CSR con Vue 3 + Vite consumiendo la API de Spring Boot. E-commerce, landing o contenido público con SEO crítico → SSR con Nuxt. Y si ya tienes JSP + Spring Boot renderizando en servidor… ya estás en la categoría SSR; piénsalo dos veces antes de migrar a CSR puro y perder eso.