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é.
La última palanca de la carga de datos no es cuándo pides, sino en qué orden entregas. Una página real mezcla datos de latencias muy distintas: la ficha del producto llega en milisegundos, sus reseñas requieren una agregación lenta. Bloquear todo el HTML hasta que lo más lento resuelva penaliza a lo rápido. El streaming de SSR invierte el reparto: el servidor emite de inmediato el esqueleto y lo crítico, mantiene la conexión abierta, y va transmitiendo cada trozo diferido en cuanto resuelve. La herramienta que dibuja esa frontera entre lo inmediato y lo diferido es Suspense, y la que marca qué no puede esperar es deferStream.
- Entender que
Suspensees la frontera de streaming: lo que queda dentro se transmite después. - Marcar los datos que deben bloquear el HTML inicial con
deferStream. - Anidar
Suspensepara crear regiones que streamean de forma independiente. - Encadenar
preloadsin esperar para que las queries lentas puedan transmitirse.
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.
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á.
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”.
// Este dato bloquea el shell: el HTML no sale hasta que resuelve
const producto = createAsync(() => getProducto(id), { deferStream: true });
// Este no lleva deferStream: el servidor streamea su Suspense sin esperarlo
const 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.
flowchart TD
R[peticion SSR] --> CR{dato critico con deferStream}
CR -->|si| W[espera a resolver antes de emitir shell]
W --> F1[primer flush: shell mas lo critico]
F1 --> FB[huecos Suspense con fallback]
FB -->|conexion abierta| ST[resenas resuelven despues]
ST --> F2[flush posterior: trozo transmitido sustituye fallback]
style W fill:#f9e2af,color:#11111b
style F1 fill:#a6e3a1,color:#11111b
style ST fill:#89b4fa,color:#11111b
style F2 fill:#a6e3a1,color:#11111bpreload 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.
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.
<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ó.
import { ErrorBoundary, Suspense } from "solid-js";
<ErrorBoundary fallback={<ErrorResenas />}>
<Suspense fallback={<EsqueletoResenas />}>
<ListaResenas resenas={resenas()} />
</Suspense>
</ErrorBoundary>
Suspense es la frontera
En SSR, lo que envuelve se emite como fallback y se transmite al resolver. Lo de fuera llega en el primer flush.
deferStream ancla lo crítico
Marca los datos que deben viajar en el HTML inicial. El servidor no emite el shell hasta que resuelven.
Regiones independientes
Suspense anidados o hermanos streamean por separado. Cada frontera es un trozo que llega cuando esta listo.
Diferir es tentador, pero no todo debe diferirse. Si envuelves el contenido principal en un Suspense sin deferStream, el HTML inicial lleva un esqueleto donde debería ir el título y el precio: los rastreadores que no ejecutan la transmisión ven un hueco, y el usuario percibe un primer pintado vacío. La regla es repartir con criterio de producto: lo que define el significado de la página —lo que un buscador debe indexar y lo que el usuario vino a ver— va con deferStream en el primer flush; lo accesorio se difiere. El streaming acelera cuando difiere lo secundario, no cuando difiere lo esencial.
Es fácil confundir el streaming con “cargar varias cosas a la vez”, pero el paralelismo es la parte trivial; lo profundo es que el streaming te da control sobre el orden en que los bytes llegan al usuario. El SSR clásico trata la página como una transacción atómica: o está entera o no está, y su tiempo hasta el primer pintado lo fija su dato más lento, por irrelevante que sea. El streaming disuelve esa atomicidad y la sustituye por una línea temporal que tú esculpes con dos cinceles complementarios. Suspense marca las junturas por donde la página puede partirse: cada frontera declara “esto puede desprenderse y llegar después”. deferStream marca, en sentido inverso, lo que se niega a desprenderse: “esto viaja en el primer flush o no hay página”. Entre ambos defines un contrato de entrega escalonada donde lo crítico ancla el HTML inicial y lo diferible fluye detrás por la conexión abierta, sustituyendo sus fallbacks a medida que la verdad resuelve en el servidor. Y todo se apoya, otra vez, en la identidad con clave: lo que streamea son queries disparadas temprano por preload sin esperarlas, resueltas en el servidor bajo su clave, y serializadas hacia el cliente que las rehidrata sin repedirlas. Dominar el streaming es, en el fondo, dejar de preguntar “cuánto tarda en cargar la página” —una pregunta sin respuesta útil cuando la página se entrega a trozos— y empezar a preguntar “qué ve el usuario en cada instante de la entrega”, que es la única métrica que de verdad experimenta.
- Monta una ficha con un dato rápido crítico y otro lento diferible, y confirma que sin streaming el HTML espera al lento.
- Marca el crítico con
deferStream: truey envuelve el lento en unSuspense; verifica que el shell sale con lo crítico y el resto se transmite después. - Dispara ambas queries en
preloadconvoidy comprueba que la lenta ya está en vuelo cuando empieza el streaming. - Añade una segunda región lenta en un
Suspensehermano y observa que cada una llega por separado sin esperar a la otra. - Mete a propósito el contenido principal en un
SuspensesindeferStream, inspecciona el HTML inicial y razona el impacto en SEO y primer pintado.