Saltar al contenido

Concurrencia, prioridad, Suspense y el rumbo de Solid 2.0

El marco conceptual que unifica el nivel: la concurrencia de Solid es cooperativa y vive en el plano asíncrono, no es el time-slicing preventivo de React que trocea el trabajo síncrono sobre la CPU. La prioridad se reduce a dos carriles, la actualización urgente que compromete de inmediato y la transición diferida que espera a lo asíncrono. Suspense y transiciones son dos mitades complementarias, el dónde esperar y el cómo esperar, que componen en lugar de competir. Y hacia dónde va Solid 2.0 con la reactividad asíncrona nativa, createAsync, projections y un store reescrito que difuminan la frontera entre Suspense y transición.

Concurrencia cooperativa, no time-slicing

En React, concurrencia significa un planificador que puede interrumpir un render largo, ceder el hilo al navegador y reanudar después: trocea el trabajo síncrono en fragmentos para que la CPU no se bloquee al pintar. Solid no hace nada de eso, y no le hace falta. Su reactividad de grano fino ya actualiza solo los nodos exactos que cambian, sin reconciliar árboles ni volver a ejecutar componentes, así que el trabajo síncrono es tan barato que no hay nada que trocear. La concurrencia de Solid vive en otro plano: el asíncrono. No interrumpe cálculos; mantiene dos versiones de una porción del grafo —la comprometida y la que se prepara— y decide cuándo canjearlas.

La palabra cooperativa es precisa: nadie interrumpe a nadie. La actualización en transición no compite por el hilo con la urgente; simplemente se aparta a una rama clonada y espera su turno de compromiso hasta que lo asíncrono resuelve. Por eso no hay startTransition que pueda hacer más fluida una animación pesada de JavaScript: eso sería trabajo síncrono, y las transiciones de Solid no tocan ese dominio. Confundir los dos modelos es el malentendido más común de quien llega desde React, y tiene una consecuencia práctica: si tu página se traba, la culpa no es de la falta de transiciones sino de un cálculo síncrono caro, y la solución es otra —memoizar, dividir el trabajo, sacarlo a un worker—, no envolverlo en startTransition.

El corolario es liberador. Como Solid no paga el impuesto del virtual DOM ni de reconciliar, casi nunca necesitas concurrencia de cómputo; la que sí necesitas, una y otra vez, es concurrencia de datos: coordinar esperas por la red sin castigar al usuario con parpadeos. Ese es exactamente el nicho que las transiciones ocupan, y explica por qué en Solid son una herramienta de uso cotidiano y no un mecanismo de rescate para rendimiento extremo.

Prioridad: dos carriles, urgente y diferido

Con ese marco, la prioridad en Solid se reduce a una distinción binaria y nítida. Una actualización urgente es la escritura normal de un signal: se propaga y se compromete de inmediato, en el mismo tick. Una actualización en transición es diferida: se propaga en una rama aparte y su compromiso espera a que lo asíncrono que dispara esté listo. No hay una escala de cinco prioridades ni carriles numerados como en el planificador de React; hay dos regímenes, y tú eliges en cuál cae cada cambio con el simple gesto de envolverlo, o no, en startTransition.

TSX
// Carril urgente: el input debe responder al instantesetTexto(e.currentTarget.value);// Carril diferido: la búsqueda pesada que ese texto dispara puede esperarstartTransition(() => setConsulta(e.currentTarget.value));

Este patrón —una escritura urgente para el feedback inmediato y otra en transición para el trabajo caro— es la forma canónica del typeahead: mantener el input fluido mientras una lista costosa se recalcula detrás. El componente completo lo deja ver:

TSX
import { createResource, createSignal, Suspense, useTransition } from "solid-js";function Typeahead() {  const [texto, setTexto] = createSignal("");  const [consulta, setConsulta] = createSignal("");  const [pending, start] = useTransition();  const [resultados] = createResource(consulta, buscar);  const escribir = (v: string) => {    setTexto(v);                              // urgente: el input no se traba    start(() => setConsulta(v));              // diferido: la lista espera  };  return (    <>      <input value={texto()} onInput={(e) => escribir(e.currentTarget.value)} />      <ul classList={{ atenuado: pending() }}>        <Suspense><Lista items={resultados()} /></Suspense>      </ul>    </>  );}

El teclado nunca se traba porque su signal —texto— está en el carril urgente; la lista no parpadea porque el suyo —consulta— está en el diferido. Fíjate en que pending() refleja solo el carril diferido: es el estado de la transición, no del input. Dos carriles, una decisión por escritura, y un único signal que anuncia la espera del que puede permitírsela.

Suspense y transiciones: el dónde y el cómo

Es tentador ver Suspense y transiciones como dos features sueltas de async. Son, en realidad, dos mitades de un solo sistema. Suspense declara el dónde: los límites del árbol en los que es legítimo esperar por datos. Una transición declara el cómo: la política de qué mostrar durante esa espera —el fallback, o el contenido anterior retenido—. Suspense sin transición muestra el fallback en cada pendiente; transición sobre Suspense convierte ese fallback en retención silenciosa. Ninguna sustituye a la otra; se necesitan.

Esa separación tiene una virtud de composición: puedes anidar límites <Suspense> para trocear la espera y una única transición los coordina a todos. Un límite exterior para la estructura de la página y uno interior para un panel que carga más lento dejan que lo rápido aparezca pronto mientras lo lento se retiene, todo bajo el mismo compromiso atómico de la transición.

TSX
<Suspense fallback={<EsqueletoPagina />}>  <Cabecera datos={usuario()} />  <Suspense fallback={<EsqueletoPanel />}>    <PanelLento datos={detalle()} />   {/* su propio límite, misma transición */}  </Suspense></Suspense>

Hacia Solid 2.0: async de primera clase

El rumbo de Solid 2.0, en desarrollo activo, empuja este modelo hasta su conclusión lógica: hacer que lo asíncrono viva dentro del grafo reactivo en lugar de coordinarse desde fuera. createAsync —ya presente en el router— apunta a esa dirección, tratando una promesa como una fuente reactiva que suspende de forma natural al leerse, sin la ceremonia de la tupla y los estados de createResource.

TSX
import { createAsync } from "@solidjs/router";// La lectura suspende sola; no orquestas recurso ni Suspense a manoconst usuario = createAsync(() => obtenerUsuario(id()));const perfil = createAsync(() => obtenerPerfil(usuario()?.id));// perfil depende de usuario: el grafo encadena las esperas por sí mismo

La reactividad asíncrona nativa persigue que una derivación pueda esperar por datos sin que tú orquestes recursos y Suspense a mano, y las projections y el store reescrito buscan que el estado derivado de fuentes async sea tan de grano fino como el síncrono lo es hoy. En ese mundo, la frontera entre Suspense y transición se difumina porque el await deja de ser un caso especial y pasa a ser una propiedad del grafo: si una lectura está pendiente, el sistema ya sabe dónde esperar y cómo coordinar el compromiso, con las transiciones como el mecanismo por defecto de esa coordinación.

No es una ruptura con lo que has aprendido en este nivel: es su generalización. Los conceptos —retener lo viejo, comprometer atómico, anunciar el pendiente— siguen intactos; lo que cambia es que dejan de requerir cableado explícito y se vuelven el comportamiento base del modelo. Aprender bien las transiciones hoy no es aprender una API que caducará: es aprender la semántica que Solid 2.0 va a volver ubicua.