Saltar al contenido

Datos diferidos y streaming con Suspense

Enviar primero lo crítico y transmitir el resto a medida que resuelve. Cómo `Suspense` es la frontera de streaming en SSR: lo que queda dentro se emite como fallback y se transmite después por la misma conexión, mientras `deferStream` marca los datos que deben bloquear el HTML inicial. Por qué `preload` dispara sin esperar para que las queries streameen, cómo anidar `Suspense` crea regiones independientes, y el reparto entre lo que el usuario necesita ya y lo que puede llegar en cuanto esté.

Crítico contra diferido: el reparto

Toda pantalla admite una pregunta: ¿qué necesita el usuario ya y qué puede llegar en cuanto esté? El título, el precio y la imagen de un producto son críticos —sin ellos la página no significa nada—. Las reseñas, las recomendaciones o el historial de precios son diferibles —enriquecen, pero pueden aparecer un instante después—. El streaming existe para honrar ese reparto: entregar lo crítico en el primer byte útil y transmitir lo diferido después, en vez de que lo lento retenga a lo rápido como rehén.

TSX
function Detalle() {  const producto = createAsync(() => getProducto(id));  // rapido: critico  const resenas = createAsync(() => getResenas(id));    // lento: diferible  // el reto es no dejar que resenas retrase la aparicion de producto}

Sin streaming, el SSR clásico espera a que toda la página resuelva antes de emitir una sola etiqueta: la reseña más lenta fija el tiempo hasta el primer pintado. El streaming rompe ese acoplamiento por regiones.

Suspense como frontera de streaming

En SSR, Suspense no es solo un fallback de carga: es la frontera de streaming. Todo lo que envuelve puede desprenderse del HTML inicial. El servidor emite el marcado de alrededor y, en el hueco del Suspense, coloca el fallback; mantiene la conexión abierta y, cuando el dato de dentro resuelve, transmite ese trozo de HTML ya hidratado que sustituye al fallback en el navegador. Lo de fuera del Suspense llega en el primer flush; lo de dentro, en cuanto está.

TSX
import { Suspense } from "solid-js";function Detalle() {  const producto = createAsync(() => getProducto(id), { deferStream: true });  const resenas = createAsync(() => getResenas(id));  return (    <>      {/* critico: bloquea el HTML inicial, llega en el primer flush */}      <h1>{producto()?.nombre}</h1>      <p>{producto()?.precio}</p>      {/* diferido: se emite como fallback y se transmite al resolver */}      <Suspense fallback={<EsqueletoResenas />}>        <ListaResenas resenas={resenas()} />      </Suspense>    </>  );}

deferStream: marcar lo que no puede esperar

Por defecto, el servidor no espera a los datos de un Suspense para emitir el shell: los streamea. Pero a veces un dato debe estar en el HTML inicial —por SEO, porque es el contenido principal, o porque un fallback ahí se vería mal—. deferStream: true en las opciones de createAsync le dice al SSR: no emitas el HTML hasta que este dato esté resuelto. Es la palanca inversa del streaming: convierte un dato de “transmítelo cuando puedas” a “bloquea el primer byte hasta tenerlo”.

TSX
// Este dato bloquea el shell: el HTML no sale hasta que resuelveconst producto = createAsync(() => getProducto(id), { deferStream: true });// Este no lleva deferStream: el servidor streamea su Suspense sin esperarloconst resenas = createAsync(() => getResenas(id));

La combinación es el instrumento afinado: deferStream para el puñado de datos críticos que deben viajar en el primer flush, y Suspense sin deferStream alrededor de todo lo diferible para que se transmita después. El reparto entre lo uno y lo otro es una decisión de producto tanto como técnica.

preload que dispara sin esperar, y Suspense anidado

El streaming solo funciona si las queries lentas ya están en vuelo cuando el servidor empieza a emitir. Por eso el preload de la lección anterior encaja aquí como una pieza: dispara las queries diferibles con void —sin esperarlas— para que arranquen temprano y estén transmitiéndose mientras el shell ya viaja. Esperarlas en preload mataría el streaming: bloquearías el HTML por lo que querías diferir.

TSX
export const route = {  preload({ params }) {    void getProducto(params.id); // critico, en vuelo    void getResenas(params.id);  // diferido, en vuelo: podra streamear  },} satisfies RouteDefinition;

Y como cada Suspense es una frontera independiente, anidarlos trocea la página en regiones que se transmiten por separado: las reseñas y las recomendaciones, en dos Suspense hermanos, llegan cada una en cuanto resuelve, sin esperarse entre sí. La granularidad de tus fronteras Suspense es la granularidad de tu streaming.

TSX
<Suspense fallback={<EsqueletoResenas />}>  <ListaResenas resenas={resenas()} /></Suspense><Suspense fallback={<EsqueletoRecomendados />}>  <Recomendados items={recomendados()} /></Suspense>

Aislar la carga tiene una contracara igual de valiosa: aislar el fallo. Envuelve una región diferida en un ErrorBoundary junto a su Suspense y, si esa query revienta, su trozo muestra un error localizado sin tumbar el resto de la página, que ya viajó en el shell. Una reseña que no carga no debería llevarse por delante el precio que sí llegó.

TSX
import { ErrorBoundary, Suspense } from "solid-js";<ErrorBoundary fallback={<ErrorResenas />}>  <Suspense fallback={<EsqueletoResenas />}>    <ListaResenas resenas={resenas()} />  </Suspense></ErrorBoundary>