Mantener la UI vieja visible mientras la nueva se prepara
El mecanismo fino de por qué no hay parpadeo: un Suspense que ya resolvió al menos una vez no vuelve a su fallback dentro de una transición, sino que retiene su último contenido comprometido mientras la rama nueva se prepara. Diferencia entre arranque en frío, sin contenido previo donde el fallback sí es correcto, y arranque en caliente, donde la transición conserva lo viejo. El commit atómico que elimina el estado intermedio y evita el tearing, y el uso de resource.latest para leer el valor anterior mientras el nuevo se busca, incluso sin envolver en transición.
Suspense frío y Suspense caliente
Un <Suspense> tiene, para lo que nos ocupa, dos regímenes. En frío nunca ha comprometido contenido: su rama interna está pendiente por primera vez y no existe ninguna vista anterior. Ahí el fallback es la respuesta correcta, y una transición no lo cambia —no hay nada viejo que sostener—. En caliente el límite ya resolvió al menos una vez y tiene hijos comprometidos en el DOM. Si en ese estado una transición dispara un nuevo pendiente, Solid decide retener lo comprometido en lugar de volver al fallback.
// Primera carga (frío): id nunca resolvió -> Suspense muestra fallbackconst [id, setId] = createSignal(1);const [usuario] = createResource(id, obtenerUsuario);// Más tarde (caliente): Ficha ya está en pantallastartTransition(() => setId(2));// Suspense NO vuelve al fallback: retiene la Ficha de id=1// hasta que obtenerUsuario(2) resuelveLa consecuencia de diseño es importante: las transiciones no mejoran el arranque inicial. Para eso están el fallback bien diseñado, el streaming del servidor o los datos precargados. Las transiciones gobiernan las actualizaciones de una vista que ya existe. Confundir ambos momentos lleva a esperar que una transición borre el spinner de la primera pantalla, cosa que jamás hará.
Merece la pena nombrar por qué el régimen caliente es seguro. Retener contenido viejo solo es honesto si ese contenido sigue siendo una respuesta válida —anticuada, pero válida— a lo que el usuario ve. Y lo es: la Ficha de id=1 describía correctamente al usuario 1, y mientras la del 2 no esté lista, seguir mostrando la del 1 con una marca de que algo cambia no engaña a nadie. El fallback, en cambio, destruye una verdad usable para poner una ausencia. La transición prefiere la verdad anticuada anotada sobre la ausencia limpia, y en la enorme mayoría de las actualizaciones esa preferencia es la correcta.
El commit atómico: sin estados a medias
El segundo pilar del cero parpadeo es que la transición se comprometa de forma atómica. Mientras dura, Solid trabaja sobre una rama clonada del grafo: aplica ahí la escritura, deja que los recursos afectados refetcheen y observa los límites <Suspense> internos. Nada de eso toca el DOM que el usuario ve. Solo cuando todos los pendientes de la rama han resuelto, se canjea la vista vieja por la nueva en un único paso.
Esa atomicidad es lo que elimina el tearing: el estado inconsistente en el que una parte de la interfaz refleja los datos nuevos y otra los viejos. Sin transición, si dos recursos que dependen del mismo id resuelven en instantes distintos, el usuario ve durante unos milisegundos una cabecera del usuario nuevo junto a una lista del viejo. La transición prohíbe ese fotograma: retiene todo lo viejo, junto, hasta que todo lo nuevo está listo, junto. La coherencia visual deja de ser un accidente afortunado del timing y pasa a ser una garantía.
Esta garantía escala con la complejidad de la vista. Cuantas más fuentes asíncronas dependan de un mismo cambio, más valiosa es la atomicidad, porque más maneras hay de que el timing produzca fotogramas inconsistentes. Un panel de detalle que carga a la vez el perfil, las estadísticas y el historial de un usuario tiene tres relojes distintos corriendo; sin transición verías el orden aleatorio en que llegan, con transición ves una sola conmutación limpia. La transición convierte tres condiciones de carrera visuales en cero.
resource.latest: el valor de ayer mientras llega el de hoy
Hay un complemento de grano más fino para cuando no quieres retener toda la vista sino solo mostrar el dato anterior de un recurso concreto mientras el nuevo se busca: resource.latest. Leer el recurso como función —datos()— suspende si está pendiente y lo captura el <Suspense>. Leer datos.latest devuelve el último valor resuelto sin suspender, así que puedes pintar los resultados viejos, atenuados, mientras el refetch corre por debajo.
const [datos] = createResource(consulta, buscar);// datos() -> suspende si está pendiente (lo atrapa Suspense)// datos.latest -> el último valor resuelto, sin suspender<ul classList={{ obsoleto: datos.loading }}> <For each={datos.latest ?? []}> {(x) => <li>{x.titulo}</li>} </For></ul>resource.latest y las transiciones no compiten: se combinan. La transición gobierna el compromiso atómico de la vista completa; latest te da control local para decidir qué recurso concreto muestra su valor previo durante la espera. En una lista con paginación es el patrón exacto: quieres los resultados de la página anterior visibles y ligeramente apagados mientras la siguiente carga.
function Paginada() { const [pagina, setPagina] = createSignal(1); const [datos] = createResource(pagina, buscarPagina); const siguiente = () => startTransition(() => setPagina((p) => p + 1)); return ( <> <ul classList={{ obsoleto: datos.loading }}> <For each={datos.latest ?? []}>{(x) => <li>{x.titulo}</li>}</For> </ul> <button onClick={siguiente} disabled={datos.loading}>Siguiente</button> </> );}El criterio para elegir entre retención total —la transición— y retención parcial —latest— es qué coherencia necesitas. Si varias piezas de la vista deben cambiar juntas o no cambiar, usa la transición y su commit atómico; si solo te importa que una lista no parpadee y el resto puede ir a su ritmo, latest es más barato y local. En la práctica se mezclan: una transición envuelve el cambio grande y latest afina el detalle de un recurso concreto dentro de ella.
La retención tiene límites: errores y esperas muy largas
Retener lo viejo asume que lo nuevo va a llegar. ¿Y si la petición falla? La transición no oculta el error: si el recurso rechaza, la rama en preparación lanza, y el <ErrorBoundary> más cercano lo captura. El contenido viejo se mantuvo hasta ese instante, así que el usuario pasa de una vista válida a un estado de error explícito, sin un fallback intermedio que prometiera datos que nunca vinieron.
<ErrorBoundary fallback={(e) => <Aviso error={e} />}> <Suspense fallback={<Skeleton />}> <Ficha datos={usuario()} /> </Suspense></ErrorBoundary>La otra frontera es la espera muy larga. Como la vista vieja no parpadea, una transición que tarda diez segundos no da, por sí sola, ninguna señal de que algo va mal; por eso el indicador de pendiente de la lección siguiente no es un lujo estético, sino el complemento necesario para que la retención no se confunda con un cuelgue.