wandres.dev
DATA LOADING · query, cache, createAsync

preload en rutas: cargar antes de renderizar

Una ruta puede exportar `preload` para calentar la caché en cuanto el usuario navega o incluso hace hover sobre un enlace, antes de que el componente se monte. Cómo se declara el objeto `route` con `satisfies RouteDefinition`, por qué `preload` dispara las queries sin esperarlas, cómo el componente Anchor precarga en hover y focus con `preloadDelay`, y qué significa cada valor de `intent` (initial, native, navigate, preload) para razonar cuándo y por qué corre la precarga.

⏱ 17 min

El patrón query más createAsync ya deduplica y cachea, pero por defecto la petición arranca cuando el componente se monta: primero renderiza, luego pide, y el usuario mira un esqueleto mientras la red trabaja. preload rompe ese orden. Una ruta declara qué datos necesita y el router los pide antes de renderizarla —en cuanto navegas, y con los enlaces del framework, ya en el mero hover—. Cuando el componente por fin se monta, su createAsync encuentra la caché caliente y pinta al instante. Es la diferencia entre pedir al llegar y pedir de camino, y es lo que hace que una app SolidStart se sienta inmediata.

🎯 Al terminar esta lección sabrás
  • Declarar preload en una ruta con satisfies RouteDefinition y calentar la caché con las queries.
  • Entender por qué preload dispara las queries sin esperarlas, y cómo el componente las relee.
  • Precargar en hover y focus con el componente A y afinar el retardo con preloadDelay.
  • Razonar el valor de intent —initial, native, navigate, preload— para saber cuándo corre la precarga.

El waterfall de renderizar y luego pedir

Sin precarga, la secuencia es una cascada: el router monta el componente, el componente ejecuta createAsync, y solo entonces sale la petición. El tiempo de red se suma después del tiempo de navegación, y si el componente tiene hijos que también piden, la cascada se alarga escalón a escalón. El usuario paga esa suma como latencia visible.

// Sin preload: la peticion no arranca hasta que el componente existe
function Detalle(props: { id: string }) {
  const producto = createAsync(() => getProducto(props.id)); // pide AL montar
  return <Suspense fallback={<Esqueleto />}>{/* ... */}</Suspense>;
}

La idea de preload es adelantar ese disparo: mover el momento en que la petición empieza desde “cuando el componente se monta” hasta “cuando el router sabe que vas hacia esa ruta”, que puede ser bastante antes.

preload: calentar la caché antes de renderizar

Una ruta exporta un objeto route con una función preload. El router la invoca al entrar en la ruta, y dentro llamas a las mismas queries que el componente usará. La clave del patrón: no esperas la promesa —la disparas y sigues—. Con eso la query queda en vuelo y su valor se deposita en la caché bajo su clave; el componente, al montarse, lo relee y lo encuentra ya resuelto o casi.

// ~/routes/productos/[id].tsx
import type { RouteDefinition } from "@solidjs/router";
import { createAsync, useParams } from "@solidjs/router";
import { getProducto } from "~/lib/productos";

export const route = {
  preload({ params }) {
    void getProducto(params.id); // dispara la query, NO la espera
  },
} satisfies RouteDefinition;

export default function Detalle() {
  const params = useParams();
  const producto = createAsync(() => getProducto(params.id)); // relee la cache caliente
  return (
    <Suspense fallback={<Esqueleto />}>
      <h1>{producto()?.nombre}</h1>
    </Suspense>
  );
}

El void es deliberado: comunica que ignoras el valor a propósito. preload no está para devolver datos —ese es el trabajo de createAsync—, sino para arrancar su carga. La única fuente de verdad sigue siendo la query cacheada; preload solo adelanta el instante en que se llena. Como createAsync y preload llaman a la misma query con la misma clave, la deduplicación garantiza que no haya doble petición: si la precarga sigue en vuelo cuando el componente lee, ambos comparten la misma promesa.

Precargar en hover con el componente A

Aquí preload despliega su verdadera potencia. El componente de enlace A del router no espera a que hagas clic: al pasar el ratón por encima o al enfocar el enlace con el teclado, ejecuta el preload de la ruta destino. Cuando el clic llega, los datos suelen estar ya en caché y la navegación se siente instantánea.

import { A } from "@solidjs/router";

// Al hacer hover o focus sobre el enlace, se dispara el preload de /productos/7
<A href={`/productos/${p.id}`}>{p.nombre}</A>

El comportamiento se gobierna de forma global y por enlace. En el Router fijas el retardo con preloadDelay —los milisegundos de hover antes de precargar, para no disparar en cada barrido del ratón— y puedes desactivarlo del todo; en un enlace concreto lo anulas con preload={false} cuando la ruta destino es cara y no quieres precargar de más.

import { Router } from "@solidjs/router";

<Router preload={true} preloadDelay={200}>
  {/* rutas */}
</Router>

<A href="/informe-pesado" preload={false}>Informe</A>
flowchart TD
H[hover o focus sobre enlace A] -->|intent preload| PL[preload de la ruta destino]
N[clic o navegacion] -->|intent navigate| PL
PL --> Q[dispara getProducto sin esperar]
Q --> C[valor en cache bajo la clave]
M[componente se monta] --> RA[createAsync relee la query]
RA --> C
C -->|cache caliente| I[render inmediato sin esperar red]
style PL fill:#89b4fa,color:#11111b
style Q fill:#f9e2af,color:#11111b
style C fill:#a6e3a1,color:#11111b

intent: por qué se dispara la precarga

preload recibe un objeto con params, location e intent, y este último te dice por qué corre en cada invocación. Distinguirlo te permite escribir precargas más finas —por ejemplo, hacer un trabajo caro solo en la navegación real y no en cada hover—.

export const route = {
  preload({ params, location, intent }) {
    void getProducto(params.id);
    if (intent === "navigate") void getResenas(params.id); // extra solo al navegar de verdad
  },
} satisfies RouteDefinition;

Los cuatro valores cubren cada forma de entrar en una ruta. initial es la carga inicial de la página, típicamente durante el SSR. native es una navegación del navegador —atrás, adelante, recargar—. navigate es una navegación del router provocada por el usuario dentro de la app. Y preload es la precarga especulativa que dispara el hover o el focus sobre un enlace A, antes de que haya ningún clic. Leer intent es leer la intención del router: la misma función se ejecuta en los cuatro casos, pero tú decides cuánto trabajo hacer en cada uno.

Un matiz que evita bugs sutiles: preload corre tanto en el servidor —en la carga inicial— como en el cliente —en cada navegación y hover posterior—. Por eso su cuerpo debe ser seguro en ambos entornos. Limítalo a disparar queries, que ya saben cruzar la frontera red y serializarse; no metas ahí acceso directo al DOM ni APIs solo de navegador, porque en el SSR no existen y la precarga fallaría en silencio justo donde más la necesitas. Como heurística: si una línea de preload no sería válida en un servidor sin ventana, no pertenece a preload.

🔥

Cache caliente

preload dispara la query antes de renderizar. Al montarse, createAsync la encuentra resuelta o en vuelo, no arranca de cero.

🖱️

Hover que precarga

El componente A ejecuta el preload destino en hover y focus. El clic llega con los datos casi listos.

🧭

intent como motivo

initial, native, navigate y preload dicen por que corre la precarga, para graduar cuanto trabajo hacer.

⚠️
No esperes en preload ni devuelvas datos desde él

El error clásico al venir de otros frameworks es tratar preload como un cargador que devuelve datos y awaitear dentro. Si esperas la promesa, vuelves a introducir el waterfall que querías eliminar: el router se queda bloqueado en preload en vez de renderizar mientras la red trabaja. Y si devuelves el dato desde preload, creas una segunda fuente de verdad que compite con la query. La regla es nítida: dentro de preload dispara las queries con void y no esperes; deja que createAsync sea el único que lee. preload arranca la carga; el componente la consume.

preload separa el momento de pedir del momento de renderizar, y ese divorcio es la velocidad

La intuición ingenua acopla dos cosas que no tienen por qué ir juntas: “necesito el dato” y “estoy renderizando el componente que lo muestra”. En la cascada por defecto, la petición no puede empezar hasta que el componente existe, porque es el componente quien la lanza. preload disuelve ese acoplamiento moviendo el disparo de la carga a un punto anterior de la línea temporal —el instante en que el router sabe que vas hacia esa ruta— que puede adelantarse muchísimo, hasta el mero hover sobre un enlace. Y funciona sin duplicar nada precisamente por lo que aprendiste en la lección anterior: como preload y createAsync invocan la misma query con la misma clave, la deduplicación las cose en una sola petición; la precarga no es una carga distinta que haya que reconciliar, es la misma identidad resuelta antes. Por eso preload no devuelve datos: no es un cargador, es un disparador. Su trabajo termina en el instante en que la promesa queda en vuelo bajo su clave; a partir de ahí, el grafo reactivo se encarga. Piénsalo como precargar el cañón mientras el usuario aún apunta: cuando por fin dispara —el clic—, el proyectil ya está en el aire. Esa separación entre pedir y renderizar, apoyada en la identidad con clave, es la mitad de por qué una app SolidStart bien hecha se siente instantánea; la otra mitad, el streaming, la verás más adelante.

⚔️ Adelanta la carga hasta el hover
  1. Añade preload a una ruta de detalle que dispare getProducto(params.id) con void, sin esperar la promesa.
  2. Enlaza a esa ruta con A, haz hover sostenido y confirma en la pestaña de red que la petición sale antes del clic.
  3. Ajusta preloadDelay en el Router y observa cómo cambia cuánto hover hace falta para disparar la precarga.
  4. Marca un enlace a una ruta cara con preload={false} y verifica que ahí el hover ya no precarga.
  5. Registra intent dentro de preload y provoca los cuatro valores —carga inicial, atrás del navegador, clic interno y hover— razonando por qué aparece cada uno.