Prefetch en navegación con preload
Cargar los datos antes de que el usuario llegue a la ruta. Cómo la propiedad `preload` de una definición de ruta dispara las queries en cuanto hay intención de navegar —al pasar el ratón o enfocar un enlace—, cómo el prefetch y el `createAsync` del componente convergen en la misma entrada de caché sin doble petición gracias a `query`, y cómo `preload` corre en paralelo con la carga del código de la ruta para que datos y JavaScript viajen juntos en vez de en serie.
Deduplicar evita pedir dos veces lo mismo; prefetch mueve la petición en el tiempo para que el dato esté listo antes de que el usuario lo pida. La palanca es la propiedad preload de una ruta: una función que el router llama en cuanto detecta intención de navegar —cuando el ratón roza un enlace o este recibe foco— mucho antes del clic. Como el prefetch invoca la misma query que luego leerá el componente, ambos convergen en la misma entrada de caché: la navegación encuentra el dato ya resuelto o en vuelo, sin doble petición. Y como preload corre en paralelo con la descarga del código de la ruta, datos y JavaScript dejan de llegar en serie.
- Declarar la propiedad
preloaden una definición de ruta para prefetch de datos. - Entender la intención de navegación: hover y foco disparan el preload antes del clic.
- Ver cómo
preloady elcreateAsyncdel componente convergen en la misma caché sin doble petición. - Reconocer que el preload paraleliza datos y código de la ruta en vez de encadenarlos.
preload en la definición de ruta
Una ruta de SolidStart puede exportar un objeto route con una función preload. El router la ejecuta antes de renderizar el componente, y le pasa los parámetros, la ubicación y la intención de la navegación. Su trabajo es sencillo: llamar a las queries que la ruta va a necesitar.
// routes/usuarios/[id].tsx
import { createAsync, type RouteDefinition } from "@solidjs/router";
import { getUsuario } from "~/data/usuarios";
export const route = {
preload: ({ params }) => getUsuario(params.id),
} satisfies RouteDefinition;
export default function UsuarioPage(props) {
const usuario = createAsync(() => getUsuario(props.params.id));
return <h1>{usuario()?.nombre}</h1>;
}
Fíjate en un detalle deliberado: preload no espera a la query, solo la invoca. No devuelve el dato al componente ni lo bloquea; se limita a arrancar la carga y calentar la caché. El componente sigue leyendo por su cuenta con createAsync. Prefetch y lectura están desacoplados, y esa separación es la clave del patrón.
El tipo RouteDefinition documenta la forma del objeto y te da autocompletado sobre el argumento que recibe preload. Y conviene saber que preload no corre solo por hover: se ejecuta también en la carga inicial de la ruta —incluido el render en el servidor bajo SSR, donde calienta la caché por petición antes de emitir el HTML— y en cada navegación en firme. El prefetch por intención es solo su modo más vistoso; el mismo gancho sirve a la primera pintada y a la hidratación.
Intención de navegación: hover y foco
Lo que hace mágico a preload no es que corra antes del render, sino cuándo el router decide llamarlo. Por defecto, el componente A del router dispara el preload de su ruta destino en cuanto hay intención de ir: al pasar el ratón por encima del enlace o al enfocarlo con el teclado. Entre ese gesto y el clic pasan decenas o cientos de milisegundos, y en esa ventana la red ya está trabajando.
import { A } from "@solidjs/router";
// Al pasar el raton o enfocar, el router llama al preload de /usuarios/7
<A href="/usuarios/7">Ver perfil</A>
El preload recibe la intención en su argumento para que puedas afinar la política: la intención preload es el prefetch especulativo por hover o foco; navigate es el preload en firme al confirmar la navegación; initial es la primera carga de la app. Puedes decidir prefetchar de forma agresiva solo en algunas y ser conservador en otras.
El comportamiento es configurable en dos niveles. En el Router decides la política global; en cada enlace A puedes forzarla o desactivarla con su prop preload, útil para enlaces que apuntan a pantallas caras que rara vez se visitan.
// Global: activa el prefetch por intencion en toda la app
<Router preload={true}>{/* rutas */}</Router>
// Local: este enlace NO prefetcha, aunque el router lo haga por defecto
<A href="/informe-pesado" preload={false}>Informe</A>
El prefetch por intención es casi gratis cuando aciertas, pero no es literalmente gratis: cada hover dispara red y trabajo de servidor. En listas larguísimas de enlaces, o en pantallas pesadas que casi nadie visita, prefetchar por defecto todo lo que el ratón roza puede saturar la red con especulación que no se cobra. Ahí conviene bajar la agresividad —preload={false} en esos enlaces, o ramificar dentro del preload según la intención— y reservar el prefetch para los destinos donde la probabilidad de clic justifique el gasto. La heurística es la de siempre: prefetcha donde el acierto es probable y el dato, caro de esperar.
sequenceDiagram participant U as Usuario participant R as Router participant Q as query cache U->>R: raton roza el enlace R->>Q: preload dispara getUsuario 7 Q-->>R: peticion en vuelo U->>R: clic al fin R->>Q: createAsync lee getUsuario 7 Q-->>R: dato ya resuelto o casi R-->>U: render sin espera visible
preload + query = sin doble petición
Aquí encaja lo del nivel anterior. El preload llama a getUsuario(params.id) y el componente hace createAsync(() => getUsuario(props.params.id)): la misma query con el mismo argumento, luego la misma clave. La deduplicación de query hace el resto. El preload arranca la petición y la cachea; cuando el componente monta y su createAsync pide la misma clave, no dispara una segunda red: recoge la entrada que el preload dejó lista.
Sin query, el prefetch sería contraproducente: pedirías el dato al hacer hover y otra vez al montar, duplicando la carga. Es la memoización por clave la que convierte dos invocaciones separadas en el tiempo en una sola petición. Por eso preload no devuelve datos: no los necesita mover, porque la caché compartida ya es el canal por el que viajan del prefetch al componente.
Una ruta suele necesitar varias piezas de datos, y preload es el sitio para calentarlas todas de golpe. Como ninguna se espera, salen en paralelo, y cada componente anidado leerá la suya por su clave sin coordinarse con los demás.
export const route = {
preload: ({ params }) => {
getUsuario(params.id); // cabecera
getPosts(params.id); // cuerpo
getSeguidores(params.id); // barra lateral
},
} satisfies RouteDefinition;
El prefetch solo ahorra si el preload y el createAsync del componente resuelven a la misma clave de query. Si el preload llama a getUsuario(params.id) pero el componente lee getPerfil(params.id) —otra query, otro nombre— calentarás una entrada que nadie consume y dispararás la petición real al montar, con el coste doblado y ningún beneficio. Comparte siempre la función de query entre la ruta y el componente, y pásale el mismo argumento derivado de los mismos parámetros. El prefetch es un pacto sobre una clave: rómpelo y trabajas de más.
Datos y código en paralelo
El segundo beneficio es más sutil y suele importar más. En una SPA clásica, navegar a una ruta encadena dos esperas: primero descarga el chunk de JavaScript de esa pantalla, y solo cuando el código corre empieza a pedir sus datos. Es una escalera: código, luego datos. preload la aplana. El router lanza el preload de una ruta a la vez que descarga su código, de modo que la red de datos y la de JavaScript avanzan en paralelo. Cuando el componente termina de cargar y monta, sus datos suelen estar ya listos, porque llevaban descargándose todo ese rato.
Esta paralelización enlaza directamente con la última lección del nivel. El waterfall más común de todos es precisamente ese —código y luego datos, en serie— y el preload lo elimina de raíz sin que tengas que pensarlo, con solo mover la invocación de las queries del cuerpo del componente a la definición de la ruta. Prefetch por intención y ausencia de waterfall inicial son, vistos de cerca, la misma jugada: declarar arriba lo que la pantalla necesitará, para que viaje junto en vez de en fila.
Intencion, no clic
El router prefetcha al detectar hover o foco sobre un enlace, ganando la ventana entre el gesto y el clic.
Misma clave, una peticion
preload y createAsync llaman a la misma query, asi que convergen en una entrada de cache: cero doble peticion.
Datos junto al codigo
El preload corre en paralelo con la descarga del chunk de la ruta, aplanando la escalera codigo luego datos.
La forma más honda de entender preload es notar lo que no hace: no bloquea, no devuelve, no lo esperas. Es una llamada que disparas y abandonas, y ahí reside toda su fuerza. Estás haciendo una apuesta especulativa sobre el futuro inmediato del usuario —“probablemente vas a hacer clic aquí”— y pagando por adelantado el coste de acertar. Si aciertas, el dato está listo cuando llega; si fallas, calentaste una entrada de caché que nadie miró y no ha roto nada, porque query la reutilizará si el usuario acaba yendo, o la dejará caducar si no. Esa asimetría —beneficio grande al acertar, coste casi nulo al fallar— es lo que hace del prefetch por intención una de las mejores relaciones esfuerzo/latencia del frontend. Pero el patrón solo funciona porque descansa sobre la identidad con clave de la lección anterior: preload no transporta datos al componente, los deposita en la caché bajo una clave que el componente, por su cuenta, resolverá a la misma entrada. Prefetch y lectura no se conocen entre sí; se citan en una clave compartida. Interioriza esto y verás que el prefetch no es una optimización aparte que se añade al final, sino la consecuencia natural de haber dado a cada dato una identidad estable: en cuanto un dato tiene nombre y alcance, adelantarlo en el tiempo es tan simple como pronunciarlo antes. La latencia deja de ser algo que sufres al navegar y pasa a ser algo que gastas mientras el usuario aún lo está pensando.
- Añade
preloada una ruta[id]que llame a la misma query que el componente lee concreateAsync; navega y confirma en red que hay una sola petición. - Observa en la pestaña de red que la petición arranca al hacer hover sobre el enlace, no al hacer clic.
- Rompe el patrón a propósito: haz que
preloaduse una query distinta a la del componente y comprueba que ahora salen dos peticiones. - Inspecciona el argumento de
preloady ramifica según la intenciónpreloadfrente anavigate, prefetchando de forma más agresiva en una que en otra. - Explica con tus palabras por qué
preloadno devuelve el dato al componente y por qué eso es una virtud, no una carencia.