prefetch y modulepreload: la navegación siguiente y el grafo de módulos
Las dos pistas que no van sobre la página actual: prefetch para lo que vendrá después, modulepreload para aplanar la cascada de importaciones, con su soporte real en 2026.
Las pistas que hemos visto hasta ahora aceleran la carga en curso. Las dos de esta lección resuelven problemas distintos: prefetch gasta ancho de banda sobrante en preparar la navegación que aún no ha ocurrido, y modulepreload aplana la cascada de importaciones que un grafo de módulos ESM produce por construcción. Comparten el hecho de que su coste se paga siempre y su beneficio es probabilístico.
- Distinguir
prefetchdepreloadpor destinatario, prioridad y almacenamiento. - Explicar por qué la partición de caché inutilizó el
prefetchentre sitios. - Aplanar una cascada de importaciones con
modulepreloady medir el efecto. - Situar el soporte real de las reglas de especulación a fecha de 2026.
prefetch: la petición para la página siguiente
prefetch declara que un recurso hará falta en una navegación futura, no en la actual. Esa diferencia de destinatario lo cambia todo.
<!-- El HTML de la ficha de producto a la que el usuario va a hacer clic. -->
<link rel="prefetch" href="/producto/42">
<!-- El JavaScript del panel, que esta pagina no usa. -->
<link rel="prefetch" href="/js/panel.js">
Cuatro propiedades lo separan de una precarga.
La prioridad es la más baja que existe. Por debajo de todo lo demás. El navegador lo trata como trabajo de relleno para cuando la red está ociosa, y ese es el diseño correcto: la página actual siempre importa más que la siguiente.
El resultado va a la caché de disco, no a una caché de memoria efímera. Por eso sobrevive a la navegación, que es justo el punto. Y por eso está sujeto a las cabeceras de caché: una respuesta con Cache-Control: no-store no se puede guardar, así que la precarga anticipada no sirve de nada. Si tu HTML lleva no-store, prefetch sobre documentos es tiempo perdido.
La petición lleva la cabecera Sec-Purpose: prefetch. El servidor puede verla y actuar: no contar la visita en analítica, no renovar la sesión, ajustar los tiempos de caché. Es la única forma que tiene el backend de distinguir una descarga especulativa de una visita real, y merece la pena mirarla en el registro antes de sacar conclusiones sobre el tráfico.
No es una función universal. A fecha de 2026, <link rel="prefetch"> sigue sin estar en Baseline porque no está implementada en todos los motores principales. Escríbelo como mejora: donde funciona ahorra tiempo, donde no, se ignora sin efecto.
Lo que la partición de caché se llevó por delante
Durante años el patrón estrella de prefetch fue el de portada que prepara el destino ajeno: un agregador de noticias precargando el artículo del medio al que enlaza. Ese patrón ya no funciona y hace tiempo que no funciona.
Los navegadores particionan la caché de HTTP por sitio de nivel superior para cerrar un canal de seguimiento entre sitios: dos páginas de dominios distintos ya no comparten entrada de caché aunque pidan exactamente la misma URL. La consecuencia directa es que un prefetch emitido desde agregador.ejemplo deja el recurso en la partición de agregador.ejemplo, y cuando el usuario navega a noticias.ejemplo esa entrada es inalcanzable. Se descarga otra vez.
La regla es tajante: prefetch solo tiene sentido dentro de tu propio sitio. Para recursos del mismo sitio en la misma partición, sigue funcionando exactamente como siempre.
Las reglas de especulación
Para la anticipación de documentos, la sucesora de prefetch es la API de reglas de especulación, que se declara como un bloque JSON dentro de un script con el tipo speculationrules:
<script type="speculationrules">
{
"prefetch": [
{
"where": { "href_matches": "/producto/*" },
"eagerness": "moderate"
}
],
"prerender": [
{
"where": { "selector_matches": ".enlace-principal" },
"eagerness": "conservative"
}
]
}
</script>
Aporta tres cosas que la etiqueta no puede dar. Un lenguaje de selección por patrón de URL o por selector CSS, en lugar de una lista de URL fija. Un control de agresividad con cuatro niveles, de immediate a conservative, donde los intermedios se disparan al pasar el ratón por encima o al empezar a pulsar. Y el prerrenderizado completo, que no solo descarga el documento sino que lo construye en segundo plano para que la navegación sea instantánea.
El soporte, a fecha de 2026, es de un solo motor: Chromium desde la versión 109. Safari tiene una implementación tras un indicador que no está activa por defecto, y Firefox ha manifestado una posición favorable a la parte de anticipación sin haberla enviado. Un navegador que no entienda el bloque lo ignora en silencio, así que es seguro añadirlo, pero no lo cuentes como una mejora para todos tus usuarios: cuéntalo para la fracción que use Chromium.
Cada documento que anticipas y el usuario no visita es una descarga íntegra tirada. Con agresividad immediate sobre una portada con cuarenta enlaces, puedes multiplicar por varias veces el tráfico de una sesión que acaba en un solo clic. Empieza siempre por moderate o conservative, que solo actúan cuando hay una señal de intención real, y limita el patrón a las rutas que de verdad concentran la navegación. Un ratio de aciertos por debajo del cincuenta por ciento suele significar que estás gastando más de lo que ahorras.
modulepreload: el problema de la cascada de importaciones
Un grafo de módulos ESM se descubre por niveles. El navegador descarga el módulo de entrada, lo parsea, encuentra sus importaciones, las pide, las parsea, encuentra las suyas, y así sucesivamente. Cada nivel de profundidad es un viaje de ida y vuelta completo que no se solapa con nada.
Con un módulo de entrada que importa un enrutador, que importa un cliente HTTP, que importa un validador, tienes cuatro viajes en serie antes de poder ejecutar la primera línea. Con una latencia de ida y vuelta de 150 milisegundos, eso son 600 milisegundos de pura espera con la red medio vacía.
modulepreload declara los módulos de los niveles profundos en el HTML, donde el escáner los ve de golpe, y convierte los cuatro viajes en serie en cuatro peticiones en paralelo:
<link rel="modulepreload" href="/js/app.js">
<link rel="modulepreload" href="/js/enrutador.js">
<link rel="modulepreload" href="/js/cliente-http.js">
<link rel="modulepreload" href="/js/validador.js">
<script type="module" src="/js/app.js"></script>
Hace algo más que descargar. El módulo entra en el mapa de módulos del documento ya descargado, parseado y compilado, listo para ejecutarse en cuanto alguien lo importe. Un preload normal deja bytes en una caché; modulepreload deja trabajo hecho.
La especificación además permite al navegador seguir las importaciones del módulo precargado y traerse el grafo entero por su cuenta. No obliga, y no todos lo hacen igual, así que declarar los niveles profundos explícitamente sigue siendo lo fiable.
Por qué no vale preload as script
Es la sustitución que parece equivalente y no lo es, por dos motivos independientes.
El modo de credenciales no coincide. Los scripts de módulo se piden en modo same-origin; una precarga con as="script" usa no-cors por defecto. Para un módulo de otro origen, los dos modos difieren y la petición real no reutiliza la precarga: doble descarga.
No se compila nada. Una precarga con as="script" deja bytes; modulepreload deja una entrada compilada en el mapa de módulos. En un archivo grande, el parseo y la compilación cuestan tiempo de hilo principal que ya no tendrás que pagar después.
<!-- MAL para un modulo de otro origen: modo de credenciales incompatible. -->
<link rel="preload" as="script" href="https://cdn.ejemplo.com/lib.mjs">
<!-- BIEN. -->
<link rel="modulepreload" href="https://cdn.ejemplo.com/lib.mjs" crossorigin>
El soporte llegó en Chrome 66 y Edge 79, en Firefox 115 y en Safari 17, tanto en escritorio como en iOS. La cobertura global medida a mediados de 2026 ronda el 93 por ciento, y en los navegadores que no lo entienden la etiqueta se ignora: el módulo se descarga cuando toque, sin error.
Un detalle de implementación que sorprende al depurar: cambiar los atributos de una etiqueta de precarga de módulo después de que se haya emitido la petición no provoca una petición nueva, porque el mapa de módulos del documento ya está poblado y volver a pedirlo no tendría sentido. Si generas estas etiquetas desde JavaScript, escríbelas con sus atributos definitivos desde el principio.
La tabla de decisión
| Pista | Para qué carga | Prioridad | Dónde acaba | Coste de equivocarse |
|---|---|---|---|---|
preload |
La página actual | La de su as |
Caché de memoria efímera | Alto: roba ancho de banda o duplica descarga |
modulepreload |
La página actual | La de un script | Mapa de módulos, ya compilado | Medio: bytes y compilación desperdiciados |
prefetch |
Una navegación futura | La mínima | Caché de disco | Bajo en tiempo, alto en datos del usuario |
preconnect |
La página actual | No descarga | Conexión abierta | Medio: conexión ociosa que se cierra sola |
dns-prefetch |
Cualquiera | No descarga | Caché de DNS | Casi nulo |
Las dos primeras filas compiten con el resto de la carga en curso, y por eso su coste de error es el más alto. Las dos últimas están desarrolladas en preconnect y dns-prefetch.
La lectura práctica de la tabla es que el riesgo crece con la inmediatez. dns-prefetch de más te cuesta una consulta de nombre. preload de más te cuesta el LCP.
El debate sobre la anticipación suele girar en torno a adivinar la navegación del usuario, que es un problema de predicción con acierto mediocre y coste garantizado. Hay un caso mucho mejor que casi nadie explota: los fragmentos de código que tu aplicación va a pedir con certeza, solo que más tarde. Piensa en una aplicación con división por rutas donde el noventa por ciento de las sesiones acaba pasando por la vista de detalle, o en un formulario en varios pasos donde el paso dos siempre viene después del paso uno. Ahí no hay que predecir nada: la probabilidad es prácticamente uno y el momento es conocido. Anticipar ese fragmento cuando la carga inicial ha terminado y el hilo principal está libre convierte una espera de trescientos milisegundos en cero, sin arriesgar ni un byte durante la ventana crítica. La diferencia con el patrón habitual es el momento, no la etiqueta: lanzarlo tras el evento de carga, o mejor todavía en un hueco de inactividad, garantiza que no compite con nada de la primera pantalla. Y como los fragmentos de tu propio empaquetador van con nombre versionado y caché inmutable, la descarga anticipada se aprovecha entera. Es la anticipación con acierto casi seguro, y es la que menos se usa porque no suena a truco.