Suspense en SSR: streaming del HTML por partes
En el servidor, Suspense se convierte en la unidad de streaming. Con renderToStream Solid manda de inmediato el shell del HTML con un placeholder en el hueco de cada límite pendiente, y a medida que los recursos resuelven va flushando chunks: el HTML real de ese límite en una plantilla, un script inline que sustituye el placeholder y los datos del recurso serializados para que el cliente no vuelva a pedirlos. Se contrastan renderToString, renderToStringAsync y renderToStream, se sigue el ciclo shell-placeholder-chunk-swap del streaming fuera de orden, se ve cómo deferStream fuerza a esperar un recurso crítico antes de mandar el shell, y las opciones onCompleteShell y onCompleteAll del stream.
Tres formas de renderizar en el servidor
Solid ofrece tres funciones de render de servidor, y elegir bien es media batalla. renderToString es síncrona: no espera a ningún recurso, así que emite el fallback de cada Suspense y devuelve el HTML al instante —rápido, pero sin datos—. renderToStringAsync espera a que todos los recursos resuelvan y entonces devuelve el HTML completo de una vez: datos listos, pero bloqueas la respuesta hasta el recurso más lento. renderToStream es el punto óptimo: manda el shell ahora y va rellenando los huecos por streaming a medida que resuelven.
| Función | Espera recursos | Salida |
|---|---|---|
renderToString |
no | HTML síncrono con los fallback |
renderToStringAsync |
sí, todos | HTML completo de una sola vez |
renderToStream |
por límite | shell ya y huecos en streaming |
import { renderToStream } from "solid-js/web";const { pipe } = renderToStream(() => <App />);pipe(respuesta); // manda el shell de inmediato y va cosiendo los huecos al resolverrenderToStream da el mejor TTFB —el tiempo hasta el primer byte— porque no espera a nada para enviar el armazón, y aun así entrega datos reales conforme llegan. Es el modo que SolidStart usa por defecto para SSR, y el que conviene salvo que tengas una razón concreta para bloquear.
El hueco y su relleno: streaming fuera de orden
El mecanismo es más simple de lo que parece. Al renderizar en streaming, cada Suspense cuyo recurso aún no resolvió emite su fallback dentro de un elemento marcado con un identificador: ese es el hueco. El shell con todos esos huecos se envía de inmediato y el navegador lo pinta —el usuario ya ve la página con sus esqueletos—. Después, cada vez que el recurso de un límite resuelve en el servidor, Solid flusha un chunk que contiene dos cosas: el HTML real de esa región dentro de una plantilla oculta, y un pequeño script inline que reemplaza el nodo del placeholder por ese contenido. Todo ello acompañado de los datos del recurso serializados, para que el cliente no tenga que volver a pedirlos.
Lo decisivo es que este relleno es fuera de orden: los chunks se envían en el orden en que los recursos resuelven, no en el orden del documento. Un límite del pie de página cuyo dato llega antes se cose antes que uno de la cabecera que tarda más, y el script inline se encarga de colocar cada trozo en su hueco correcto sea cual sea el orden de llegada. Cada Suspense es, por tanto, una costura independiente: un punto del HTML que puede completarse por su cuenta sin bloquear a los demás.
Hidratación sin doble fetch y deferStream
El detalle que cierra el círculo son los datos serializados. Cuando el cliente hidrata la página, no vuelve a ejecutar los fetch de los recursos: los lee de la carga que Solid inyectó en el HTML junto a cada chunk. Sin ese puente, cada recurso se pediría dos veces —una en el servidor para el SSR y otra en el cliente al hidratar—, desperdiciando la petición del servidor. La serialización hace que el trabajo asíncrono del servidor se reutilice íntegro en el cliente, y la hidratación se limita a reconectar la reactividad sobre un HTML que ya trae sus valores.
import { createResource } from "solid-js";// deferStream fuerza al stream a esperar este recurso antes de mandar el shell.const [critico] = createResource(cargarCritico, { deferStream: true });A veces no quieres que un dato viaje como hueco a rellenar sino que forme parte del shell inicial —contenido sobre el pliegue, algo crítico para SEO o para evitar un salto visible—. Para eso, createResource acepta la opción deferStream: true, que le dice al stream: «no mandes el armazón hasta que yo haya resuelto». Ese recurso deja de ser fuera-de-orden y bloquea el primer flush, entrando ya resuelto en el HTML inicial. Es la válvula para elegir, recurso a recurso, qué entra en el shell y qué se cose después.
En SolidStart no orquestas la serialización a mano: sus primitivas de datos ya la hacen. Un createAsync bajo un Suspense se transmite, se cose y se hidrata sin doble fetch, con la caché por petición del router de por medio:
import { createAsync } from "@solidjs/router";import { Suspense } from "solid-js";function Ficha(props: { id: string }) { const dato = createAsync(() => obtenerFicha(props.id)); // se serializa solo return ( <Suspense fallback={<Esqueleto />}> <h1>{dato()?.titulo}</h1> </Suspense> );}Afinar el stream: onCompleteShell y onCompleteAll
renderToStream acepta un segundo argumento de opciones que da control fino sobre el flujo. onCompleteShell se dispara cuando el armazón síncrono está listo para enviarse —el punto en que arranca el streaming—, y onCompleteAll cuando todos los límites han resuelto y cosido su contenido, útil para cerrar la respuesta o registrar métricas. Un nonce propaga la política de seguridad de contenido a los scripts inline que insertan los chunks.
const { pipe } = renderToStream(() => <App />, { nonce: req.nonce, onCompleteShell() { respuesta.write("<!-- shell listo -->"); // el armazón ya viaja }, onCompleteAll() { respuesta.end(); // todos los huecos cosidos; cierra el stream },});pipe(respuesta);Opción de renderToStream |
Para qué sirve |
|---|---|
nonce |
propaga la CSP a los scripts inline de los chunks |
onCompleteShell |
se dispara al emitir el armazón síncrono |
onCompleteAll |
se dispara cuando todos los límites cosieron su contenido |
renderId |
prefija ids para varios renders en una misma página |
Estas asas te dejan orquestar el ciclo completo sin adivinar tiempos: sabes exactamente cuándo salió el shell y cuándo terminó la última costura. En SolidStart rara vez las tocas a mano —el framework las gobierna— pero conocerlas revela que el streaming no es magia opaca: es un shell que se emite en onCompleteShell y una serie de chunks que terminan en onCompleteAll, con Suspense marcando cada punto donde el flujo se puede cortar.