Saltar al contenido

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
TSX
import { renderToStream } from "solid-js/web";const { pipe } = renderToStream(() => <App />);pipe(respuesta); // manda el shell de inmediato y va cosiendo los huecos al resolver

renderToStream 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.

TSX
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:

TSX
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.

TSX
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.