wandres.dev
SUSPENSE · coordinar la carga

Coordinar varios recursos y evitar la cascada de spinners

Un solo Suspense puede esperar a muchos recursos a la vez: su contador suma todas las lecturas pendientes y revela todo junto, evitando el goteo de indicadores sueltos y el salto de layout. El enemigo real no es el límite sino la topología de dependencias: si el origen de un recurso lee el resultado de otro, sus fetch se serializan en cascada y el tiempo total es la suma en vez del máximo. La cura es separar orígenes para que carguen en paralelo, leer el grafo de orígenes de una pantalla con varios recursos, y anidar Suspense cuando prefieras revelar por regiones independientes en lugar de todo a la vez.

⏱ 16 min

El poder de Suspense no está en esperar un recurso, sino en coordinar varios. Bajo un mismo límite, media docena de recursos comparten un único fallback y aparecen juntos, sin el efecto palomita de spinners que estallan a destiempo. Pero esa coordinación destapa una trampa que no es del límite sino de cómo declaraste los recursos: si el origen de uno depende del resultado de otro, se cargan en cascada —uno detrás de otro— y el usuario espera la suma de todas las latencias. Esta lección enseña a hacer que carguen en paralelo, a leer el grafo de orígenes de una pantalla real, y a decidir cuándo revelar todo junto y cuándo por regiones.

🎯 Al terminar esta lección sabrás
  • Coordinar varios recursos bajo un único Suspense para un revelado conjunto.
  • Reconocer la cascada: un recurso cuyo origen depende del resultado de otro se serializa.
  • Disparar los fetch en paralelo separando las dependencias de origen.
  • Anidar Suspense para revelar por regiones independientes cuando conviene.

Un solo límite, un revelado conjunto

El contador de un Suspense suma todas las lecturas pendientes de su subárbol. Si tres recursos se leen bajo el mismo límite, el fallback permanece hasta que los tres resuelven, y entonces los hijos aparecen de golpe. Esto es exactamente lo que quieres para una sección cohesiva —una cabecera, su avatar y sus estadísticas—: nada de que el nombre aparezca, luego la foto salte y después el contador empuje el layout. Un límite, un momento de revelado.

import { createResource, Suspense } from "solid-js";

function Panel(props: { id: string }) {
  const [perfil] = createResource(() => props.id, cargarPerfil);
  const [stats] = createResource(() => props.id, cargarStats);
  return (
    <Suspense fallback={<EsqueletoPanel />}>
      <Cabecera datos={perfil()} />
      <Metricas datos={stats()} />
    </Suspense>
  );
}

Ambos recursos toman props.id como origen de forma independiente: ninguno lee el resultado del otro. Por eso sus fetch arrancan a la vez y el fallback dura lo que tarde el más lento, no la suma de los dos. Ese detalle —orígenes independientes— es la frontera entre un panel que carga en paralelo y uno que se arrastra en cascada.

La cascada: cuando un recurso espera a otro

La cascada aparece en cuanto el origen de un recurso lee el valor de otro. El origen de un createResource se re-evalúa reactivamente; si dentro llamas al accessor de otro recurso, el segundo no puede ni empezar hasta que el primero resuelva, porque su origen es undefined mientras tanto. El resultado es una fila de esperas encadenadas.

// CASCADA: posts no arranca hasta que usuario resuelve, porque su origen lo lee.
const [usuario] = createResource(() => props.id, cargarUsuario);
const [posts] = createResource(() => usuario()?.equipoId, cargarPosts);

// PARALELO: ambos parten de props.id y cargan a la vez.
const [usuario2] = createResource(() => props.id, cargarUsuario);
const [posts2] = createResource(() => props.id, cargarPostsDeUsuario);
flowchart TD
S[un Suspense sobre dos recursos] --> D[el origen de uno lee el resultado del otro]
D -->|si| C[cascada usuario y luego posts suma los tiempos]
D -->|no| P[paralelo usuario y posts a la vez maximo de los tiempos]
style C fill:#f38ba8,color:#11111b
style P fill:#a6e3a1,color:#11111b

A veces la cascada es inevitable y correcta: si de verdad necesitas el equipoId que solo trae el usuario para pedir los posts, no hay atajo. Pero muchas cascadas son accidentales —pediste juntos datos que podían pedirse por separado— y cuestan latencia gratis. La regla es afirmativa: haz que cada recurso arranque desde el dato que ya tienes, no desde el que otro recurso aún está trayendo.

Tres recursos: el grafo de orígenes

Con dos recursos el análisis es fácil; con tres o cuatro conviene dibujar mentalmente el grafo de orígenes —qué lee cada fetch— antes de escribirlo. Imagina una vista de proyecto: el proyecto por su id, sus miembros por ese mismo id, y los comentarios que sí requieren el id del hilo que solo trae el proyecto. Dos ramas paralelas y una dependencia real.

function Proyecto(props: { id: string }) {
  const [proyecto] = createResource(() => props.id, cargarProyecto);
  const [miembros] = createResource(() => props.id, cargarMiembros);      // paralelo
  const [comentarios] = createResource(
    () => proyecto()?.hiloId,                                             // dependencia real
    cargarComentarios,
  );
  return (
    <Suspense fallback={<EsqueletoProyecto />}>
      <Ficha proyecto={proyecto()} miembros={miembros()} />
      <Hilo comentarios={comentarios()} />
    </Suspense>
  );
}
Recurso Origen Arranca
proyecto props.id de inmediato
miembros props.id de inmediato, en paralelo con proyecto
comentarios proyecto()?.hiloId solo tras resolver proyecto

Aquí proyecto y miembros arrancan juntos; comentarios espera solo a proyecto, no a miembros. El tiempo total no es la suma de los tres, sino el del camino más largo del grafo: proyecto seguido de comentarios. Leer así los orígenes te dice de un vistazo dónde está la ruta crítica y qué dependencias podrías romper para acortarla —quizá el servidor ya te da el hiloId en la misma respuesta del proyecto, y la cascada desaparece—.

Anidar Suspense para revelar por regiones

Un único límite tiene una contrapartida: todo espera a lo más lento. Si una parte de la página tiene datos rápidos y otra depende de un servicio pesado, coordinarlos en el mismo Suspense toma como rehén al contenido veloz. La solución es anidar límites: un Suspense externo para el armazón que llega pronto y límites internos para las regiones lentas, cada uno con su propio fallback.

<Suspense fallback={<EsqueletoPagina />}>
  <Cabecera datos={perfil()} />
  <Suspense fallback={<EsqueletoFeed />}>
    <Feed datos={feedLento()} />
  </Suspense>
</Suspense>;

Ahora la cabecera se revela en cuanto su recurso está listo, mientras el feed sigue mostrando su propio esqueleto. Un límite interno que suspende no re-suspende al externo: una vez revelado, un Suspense padre no vuelve a ocultarse porque un hijo suyo entre en carga. Eliges así la granularidad del revelado —todo junto con un límite, por regiones con varios— según qué experiencia quieras. La coordinación es una herramienta de diseño, no un ajuste técnico.

🤝

Orígenes independientes

Parte cada recurso del dato que ya posees para que los fetch arranquen a la vez.

🪜

Cascada consciente

Si un origen debe leer otro recurso, aceptas una espera secuencial: que sea a propósito.

🧩

Límites anidados

Un Suspense por región deja que lo rápido aparezca sin esperar a lo lento.

⏱️

Camino crítico

El tiempo total es el camino más largo del grafo de orígenes, no la suma de todos los recursos.

💡
El origen undefined es la señal de una dependencia

Un truco para detectar cascadas al leer código: mira el origen de cada createResource. Si es una función que llama al accessor de otro recurso() => otro()?.campo—, ese recurso está encadenado y no arrancará hasta que el otro resuelva, porque mientras tanto su origen vale undefined y un origen undefined le dice al recurso que no cargue todavía. Ese ?. revelador es la firma visual de una dependencia. Cuando el origen parte de una prop o un signal propio —() => props.id—, el recurso es independiente y carga desde el primer instante. Aprende a leer los orígenes y verás el grafo de latencia sin ejecutar nada.

⚠️
Encadenar orígenes es la fuga de latencia más silenciosa

El coste de una cascada no se ve en el código: createResource(() => a()?.x, cargarB) parece tan inocente como su versión paralela. Pero has convertido dos peticiones concurrentes en dos secuenciales, y el usuario paga la suma. En una pantalla con tres o cuatro recursos encadenados sin necesidad, la diferencia entre paralelo y cascada puede ser de cientos de milisegundos de espera regalada. Antes de que el origen de un recurso lea a otro, pregúntate si de verdad necesita ese dato o si podías pedir ambos desde la misma clave. La mayoría de las cascadas accidentales se disuelven con esa sola pregunta.

📝
Las primitivas del router deduplican por petición

Si en vez de un createResource casero usas createAsync con el query del router, la deduplicación y la caché por petición vienen dadas: dos componentes que piden el mismo dato bajo el mismo límite comparten una sola carga sin que tú coordines nada. La coordinación de recursos que aquí haces a mano —una clave, una petición— el framework la industrializa. Aun así, el análisis de cascada frente a paralelo sigue siendo tuyo: ninguna caché arregla un origen que lee a otro recurso sin necesitarlo.

El Suspense reparte el revelado; la topología decide el tiempo

Hay dos ejes en juego y confundirlos lleva a optimizar el equivocado. El primero es cuándo aparece el contenido: eso lo gobierna la forma de tus límites —un Suspense que coordina revela todo junto; varios anidados revelan por regiones—. El segundo es cuánto se tarda en tener los datos: eso no lo toca Suspense en absoluto, lo dicta la topología de dependencias entre tus recursos. Puedes tener el reparto de revelado más elegante del mundo y aun así arrastrarte, porque encadenaste orígenes y tus peticiones corren en fila india. Y al revés: recursos perfectamente paralelos mal repartidos en límites hacen que lo rápido espere a lo lento sin razón. El dominio consiste en tratarlos por separado. Primero arregla el tiempo: mira el grafo de orígenes y asegúrate de que todo lo que puede cargar en paralelo lo hace, dejando en cascada solo las dependencias reales de datos, porque el tiempo total es la longitud del camino más largo de ese grafo, no la suma de sus nodos. Después reparte el revelado: agrupa en un límite lo que debe aparecer junto y aísla en límites propios lo que puede aparecer antes. Suspense coreografía la aparición; la estructura de tus recursos decide la latencia. Quien mezcla ambos culpa al límite de una lentitud que nació tres líneas antes, en un origen que leyó a otro recurso sin necesitarlo.

⚔️ Paraleliza y luego reparte
  1. Monta un panel con dos recursos que parten de la misma clave y confirma, con latencias artificiales, que el fallback dura lo que el más lento y no la suma.
  2. Reescribe uno para que su origen lea el resultado del otro y mide cómo el tiempo total pasa a ser la suma; identifica la lectura culpable.
  3. Dibuja el grafo de orígenes de una vista con tres recursos —dos paralelos y uno dependiente— y predice el tiempo total como el camino más largo.
  4. Añade una región lenta y anídala en su propio Suspense para que la cabecera rápida se revele sin esperarla.
  5. Verifica que, una vez revelado el límite externo, un hijo interno que vuelve a suspender no reoculta la cabecera ya visible.