CSR: el modo SPA
Apagar el servidor con ssr false convierte a SolidStart en una SPA pura: el servidor entrega un shell casi vacío y todo el render ocurre en el cliente tras descargar y ejecutar el bundle. Esta lección explica qué cambia exactamente al desactivar SSR —mount deja de hidratar y pasa a renderizar de cero—, desmonta el mito de que un SPA tiene mal TTFB (lo tiene excelente: el coste se desplaza a FCP, LCP y TTI), enumera el precio real en SEO y arranque, y delimita los escenarios donde un SPA puro es la decisión correcta: apps tras login, herramientas internas, dashboards sin necesidad de indexación. Incluye clientOnly para sacar un solo componente del SSR sin apagarlo entero.
El modo por defecto de SolidStart renderiza en el servidor y luego hidrata; pero un solo interruptor —ssr: false— apaga esa mitad y te devuelve a la arquitectura clásica de la web moderna: el Client-Side Rendering. El servidor deja de componer HTML y se limita a servir un shell casi vacío; el navegador descarga el bundle, lo ejecuta y construye la interfaz desde cero. No es un modo «peor»: es un modo con un perfil de costes distinto, brillante para unos escenarios y venenoso para otros. Esta lección disecciona qué cambia al pulsarlo y cuándo pulsarlo es lo correcto.
- Configurar el modo SPA con
ssr: falsey entender qué hacemountsin HTML previo. - Corregir el mito del TTFB: en un SPA es excelente, y el coste se desplaza a FCP, LCP y TTI.
- Enumerar el precio real en SEO y en la cascada de arranque del cliente.
- Delimitar cuándo un SPA puro es la decisión correcta y usar
clientOnlypara casos mixtos.
Apagar el servidor: ssr false
Convertir SolidStart en una SPA es una línea de configuración. En app.config.ts desactivas el render de servidor y el framework reorganiza toda su cadena de entrega.
// app.config.ts
import { defineConfig } from "@solidjs/start/config";
export default defineConfig({
ssr: false, // modo SPA: sin render de servidor
});
Con esto, el servidor deja de ejecutar tus componentes. Emite un documento mínimo —el <head>, un <div id="app"> vacío y los <script> del bundle— idéntico para toda ruta. El mismo mount del cliente que en modo SSR hidrataba el HTML existente ahora, al no encontrar nada que hidratar, renderiza de cero sobre el contenedor vacío.
// src/entry-client.tsx — el mismo codigo sirve para ambos modos
import { mount, StartClient } from "@solidjs/start/client";
// Con SSR: hidrata el HTML del servidor. Sin SSR: renderiza desde cero.
mount(() => <StartClient />, document.getElementById("app")!);
La consecuencia arquitectónica es limpia: tu aplicación se vuelve un artefacto puramente estático. No hay proceso de render por petición, no hay estado de servidor que aislar, no hay getRequestEvent. Puedes desplegar el resultado en cualquier CDN o hosting estático, porque servir la app es servir tres ficheros iguales para todos.
Lo notable es que tu código apenas cambia: los mismos componentes, el mismo mount, el mismo router. Solo se mueve la frontera de dónde corren. Esa simetría es la que permitirá, más adelante, decidir el modo por ruta sin reescribir nada —un componente no sabe ni le importa si lo renderizó un servidor o el navegador, y esa ignorancia deliberada es lo que hace portátil tu interfaz entre modos—.
Un aviso para no llevarse una sorpresa: en modo SPA desaparecen las server functions ejecutadas por petición. El código que marques como de servidor sigue existiendo, pero sin un proceso de render que lo invoque en cada visita, tu lógica de datos pasa a vivir en llamadas desde el cliente a una API. No es una limitación de Solid, sino la consecuencia directa de haber apagado el render de servidor: si no hay render en el servidor, no hay dónde correr la lógica de servidor acoplada a ese render.
Esto reordena tu arquitectura de datos, no la elimina. En un SSR el mismo código podía leer la base directamente durante el render; en un SPA esa lectura se convierte en una llamada de red autenticada desde el cliente a un endpoint. Muchos proyectos conviven perfectamente así —un SPA contra una API REST o GraphQL es una arquitectura clásica y sólida—, pero conviene decidirlo a conciencia, y no descubrirlo a mitad de camino cuando ya asumías un servidor de render que ya no está.
Un matiz de implementación que conviene tener claro: ssr: false es un interruptor global. SolidStart no ofrece hoy un modo SPA por ruta tan limpio como su prerender por ruta, así que «ser un SPA» tiende a ser una decisión de aplicación, no de una ruta suelta. Lo que sí puedes hacer con grano fino es lo contrario —extraer piezas concretas del SSR con clientOnly dentro de una app que por lo demás renderiza en el servidor—, que suele ser lo que en realidad necesitas.
El coste no está en el TTFB: está después
Aquí vive el malentendido más extendido. Se dice que «el SPA tiene mal TTFB», y es falso: el TTFB de un SPA es excelente, porque el servidor devuelve un shell estático desde la caché o el CDN sin computar nada. El problema no es cuándo llega el primer byte, sino cuánto tarda ese primer byte en convertirse en algo útil. El coste no se elimina: se desplaza más allá del TTFB, a la cadena de arranque del cliente.
flowchart TD A[Navegador pide la ruta] --> B[servidor devuelve shell casi vacio] B --> C[descarga del bundle JS] C --> D[ejecuta y monta la app] D --> E[recien ahora pide los datos] E --> F[primer contenido util: LCP tardio] style B fill:#a6e3a1,color:#11111b style F fill:#f38ba8,color:#11111b
Esa cadena es una cascada estrictamente serial: hasta que el bundle no se descarga no se ejecuta, hasta que no se ejecuta no se monta la app, y hasta que no se monta no arrancan las peticiones de datos. El usuario contempla una pantalla en blanco durante la suma de todos esos tramos. Las métricas que se degradan no son el TTFB sino el FCP —cuándo aparece el primer píxel—, el LCP —cuándo aparece el contenido principal— y el TTI —cuándo la página responde—. En redes lentas o dispositivos modestos, ese intervalo se estira sin piedad.
El matiz importa porque cambia dónde intervienes para mejorar. Si creyeras que el problema es el TTFB, buscarías un servidor más rápido y no moverías la aguja ni un milímetro. El cuello real vive en la cadena del cliente, así que las palancas son otras: adelgazar el bundle, dividir el código por ruta para no cargar lo que no se usa, y disparar las peticiones de datos en paralelo al arranque en lugar de después de montar. Un SPA bien afinado no elimina la cascada —es inherente al modelo—, pero la comprime hasta hacerla tolerable; uno descuidado la deja crecer hasta que el visitante se marcha antes del primer píxel.
Conviene también medir la métrica correcta. El informe de un SPA suele mostrar un TTFB envidiable junto a un LCP pobre, y quien solo mira el primero se felicita por una app que en realidad tarda en ser útil. La métrica honesta para un SPA es el TTI y el LCP en un dispositivo modesto con la caché fría —el escenario del visitante nuevo—, no el TTFB en tu portátil con el bundle ya cargado. Optimizar lo que no duele es la forma más común de creer que un SPA va rápido mientras el usuario real espera.
Un rastreador que no ejecuta JavaScript recibe el shell vacío: sin título real, sin texto, sin datos estructurados. Google puede renderizar JS, pero lo hace en una segunda pasada diferida y con presupuesto limitado; muchos otros bots y, sobre todo, los scrapers de previsualización social —los que generan la tarjeta de un enlace compartido— no ejecutan nada y ven el hueco. Para contenido que depende de indexarse o de compartirse bien, el modo SPA no es una degradación gradual: es una barrera. Si la ruta necesita SEO, no es candidata a SPA.
Hay paliativos parciales —prerenderizar el shell con algo de contenido, servir metadatos por una ruta aparte para los scrapers—, pero son parches que devuelven, poco a poco, la complejidad que el SPA prometía evitar. Cuando te descubres reintroduciendo render de servidor para arreglar el SEO de un SPA, la señal es inequívoca: esa ruta pedía SSR desde el principio, y estabas usando el modo equivocado.
Cuándo un SPA puro tiene sentido
El modo SPA no es un error a evitar: es la herramienta correcta cuando sus costes no aplican. Y hay una familia entera de aplicaciones donde no aplican en absoluto —porque viven tras un login, no necesitan indexarse y su valor está en la interacción sostenida, no en el primer pintado—.
Apps tras autenticacion
Un panel al que solo se entra con sesion no necesita SEO: nadie lo indexa. El shell vacio no cuesta nada aqui.
Herramientas internas
Editores, consolas de administracion, back-offices. Publico conocido, sesiones largas, cero necesidad de rastreo.
Dashboards muy interactivos
Cuando la vista es un lienzo que se manipula sin recargar, el arranque unico se amortiza en minutos de uso.
El criterio se resume en tres preguntas: ¿alguien externo necesita indexar o previsualizar esta ruta? ¿Importa el primer pintado, o el usuario ya espera un arranque? ¿La sesión es larga y muy interactiva? Si las respuestas son «no, no importa y sí», el SPA no solo es aceptable: es la opción más simple y a menudo la más barata de operar, porque elimina el servidor de render entero.
Ese corolario operativo suele inclinar la balanza más de lo que se admite. Sin proceso de render por petición no hay servidor que escalar bajo carga, no hay estado que se filtre entre usuarios, no hay un endpoint que una consulta lenta pueda tumbar; el despliegue se reduce a subir estáticos a un CDN. Para un equipo pequeño que construye una herramienta interna, esa simplicidad vale a menudo más que los cientos de milisegundos de primer pintado que nadie del público llegará a medir. Elegir SPA no es solo una decisión de rendimiento percibido: es también una decisión de coste de operación.
Nada de esto convierte al SPA en una elección universal; lo mantiene como una elección específica. La misma ausencia de servidor de render que lo abarata es la que le impide personalizar el primer HTML u ocultar un secreto del cliente. Es una herramienta con un filo y un mango bien definidos: brillante cuando la agarras por donde debe —una app privada, interactiva, de sesión larga—, cortante cuando la fuerzas sobre una ruta pública que vive de su primer pintado.
Y como toda herramienta afilada, conviene sujetarla igual en equipo: dejar escrito por qué esta app es un SPA evita que, tres meses después, alguien añada dentro de ella una ruta pública de marketing y se pregunte, desconcertado, por qué no la indexa nadie. La decisión de modo, cuando es global, merece vivir documentada donde el siguiente la encuentre.
Y no tienes que elegir todo o nada. En una app con SSR puedes sacar un único componente problemático —uno que dependa de APIs del navegador o de una librería que no corre en el servidor— del render de servidor con clientOnly, sin apagar el SSR del resto.
import { clientOnly } from "@solidjs/start";
// Este componente solo se renderiza en el cliente; el servidor lo omite.
const Mapa = clientOnly(() => import("~/components/Mapa"));
export default function Ruta() {
return <Mapa lat={40.4} lon={-3.7} />;
}
Después del arranque: el pago único
La cara amable del SPA aparece una vez pagado el arranque. Cuando el bundle ya está en memoria, navegar entre rutas no vuelve a tocar el servidor: el router intercepta el enlace, resuelve la vista en el cliente y cambia la pantalla sin recargar ni pedir HTML nuevo. Las transiciones son instantáneas y el estado en memoria sobrevive a la navegación.
import { A } from "@solidjs/router";
// Tras el arranque, este enlace NO recarga: el router cambia la vista en el cliente.
<A href="/ajustes">Ajustes</A>;
Ese es el trato del CSR en una frase: pagas caro una vez, al principio, y luego cada interacción es barata. Para una sesión larga —un editor abierto una hora, un panel que se usa toda la jornada— ese coste inicial se diluye hasta volverse irrelevante, y a cambio obtienes una fluidez que un servidor consultado en cada navegación difícilmente iguala. El SPA no es «la web lenta»; es la web que concentra su lentitud en el primer segundo para regalar velocidad en todos los siguientes. La pregunta, una vez más, no es si el modelo es bueno o malo en abstracto, sino si el visitante de esta ruta se quedará el tiempo suficiente para cobrar el dividendo de ese pago único.
Hay incluso un territorio donde el SPA no es una concesión sino la única opción razonable: las aplicaciones que deben funcionar sin conexión. Una PWA que cachea su bundle y sus datos en el dispositivo vive, por definición, en el cliente; un modelo que dependa de consultar el servidor en cada navegación no puede ofrecer esa experiencia offline. Ahí el CSR deja de competir con el SSR y pasa a ser condición de posibilidad —no se elige por preferencia, se impone por requisito—.
Y aun sin requisito offline, la navegación instantánea tiene un valor psicológico difícil de cuantificar pero real: una app que responde sin recargas se siente «nativa», y esa percepción sostiene la retención en herramientas de uso diario. El primer pintado se sufre una vez; la fluidez se disfruta en cada clic. Para el software que alguien abre cada mañana y no cierra en horas, ese reparto es exactamente el que quieres.
Nada de esto redime al SPA para una landing pública ni lo condena para un panel privado; lo sitúa. Es la arquitectura de la web que se usa, no de la web que se visita. Cuanto más se parezca tu ruta a una aplicación que alguien abre y habita, y menos a una página que alguien encuentra y ojea, más se inclina la balanza hacia el cliente —y esa distinción, no una preferencia de estilo, es la que debe decidir—.
Todo modo de renderizado responde en el fondo a una sola pregunta —¿quién ejecuta el trabajo de construir la interfaz y en qué momento?— y el CSR da la respuesta más extrema: lo ejecuta el cliente, entero, después de cargar. Esa respuesta tiene un reverso luminoso que se olvida cuando solo se miran las métricas de primer pintado. Al mover el render al cliente, el servidor deja de tener trabajo por petición: se vuelve un repartidor de ficheros estáticos, infinitamente cacheable, trivial de escalar, imposible de tumbar por una consulta lenta. No hay estado de servidor que filtrar entre usuarios, no hay proceso de render que perfilar, no hay TTFB que dependa de tu base de datos. Para una clase concreta de aplicaciones —las que viven tras un muro de sesión, donde nadie de fuera mira y el usuario ya aceptó esperar un arranque a cambio de una experiencia fluida después— ese reparto de costes es exactamente el correcto, y montar SSR encima sería añadir complejidad operativa para proteger un primer pintado que a nadie le importa. El error no es usar CSR; el error es usarlo por inercia en una landing pública que vive o muere por su LCP y su indexación, y descubrir tarde que trasladaste todo el coste al peor momento posible: el del visitante que llega frío, sin bundle en caché, a decidir en dos segundos si se queda. La madurez no consiste en preferir SSR ni en preferir SPA, sino en saber que la elección es una asignación de costes —render en servidor o en cliente, trabajo por petición o arranque único, TTFB caro o FCP caro— y en hacer esa asignación ruta por ruta, según quién mira y qué espera.
- Activa
ssr: falseenapp.config.tsy comprueba en el HTML servido que el<div id="app">llega vacío, idéntico para toda ruta. - Con la red simulada a 3G lenta, mide FCP y LCP en modo SPA y en modo SSR de la misma vista; explica por qué el TTFB apenas cambia y el LCP se dispara.
- Comparte un enlace de tu SPA en una herramienta de previsualización de tarjetas sociales y observa que no hay título ni imagen; razona por qué el scraper no ve nada.
- Devuelve la app a SSR y saca un solo componente dependiente del navegador con
clientOnly; verifica que el servidor omite ese subárbol y el resto sí se renderiza. - Clasifica cinco rutas reales —landing, blog, panel tras login, editor interno, ficha de producto— en «SPA» o «no SPA» aplicando las tres preguntas de indexación, primer pintado e interactividad.