Ir al contenido

Prefetch

Montar una micro-app implica una ida y vuelta por la red. El prefetch adelanta esa ida y vuelta a un momento en el que el usuario no está esperando: mientras pasa el ratón por el enlace, mientras la sección entra en pantalla, mientras el hilo principal está en reposo.

No se ejecuta nada. El prefetch solo precalienta la caché HTTP con la URL del módulo de la app, para que el mount() posterior lo encuentre en local.

import { prefetch, prefetchAll } from 'wu-framework';
// o wu.prefetch / wu.prefetchAll
prefetch(appNames: string | string[], options?: {
on?: 'immediate' | 'hover' | 'visible' | 'idle';
target?: string | Element;
eagerness?: 'conservative' | 'moderate' | 'eager';
}): Promise<void> | (() => void)
Opción Por defecto Significado
on 'immediate' Cuándo dispararlo.
target Elemento o selector. Obligatorio para hover y visible.
eagerness 'moderate' Se pasa a la Speculation Rules API cuando está disponible. Se ignora en caso contrario.

Esto es deliberado y es lo que hay que hacer bien:

Disparador Devuelve
'immediate' Promise<void> — hazle await
'hover', 'visible', 'idle' Una función de limpieza — llámala para cancelar
// Inmediato: devuelve una Promise
await wu.prefetch('cart');
// Diferido: devuelve una función de limpieza
const cancel = wu.prefetch('cart', { on: 'hover', target: '#cart-link' });
// más tarde, p. ej. al desmontar el componente
cancel();

prefetch() no es una función async a propósito. Marcarla como async envolvería en una Promise la función de limpieza de las ramas diferidas, y el patrón documentado const off = wu.prefetch(...) / off() fallaría con “off is not a function”. La rama inmediata sigue devolviendo una Promise desde dentro, así que await sigue funcionando.

Caso límite que conviene señalar. Un disparador desconocido registra [WuPrefetch] Unknown trigger "<x>", using immediate y cae al inmediato, lo que significa que devuelve una Promise, no una función. Una errata en on: convierte tu cancel() en un TypeError.

immediate — se dispara ya.

await wu.prefetch(['profile', 'settings']);

hover — escucha mouseenter y focusin sobre el destino (ambos pasivos, así que quien usa teclado también lo obtiene). Se dispara una vez y luego elimina él mismo ambos listeners.

const off = wu.prefetch('cart', { on: 'hover', target: '#cart-link' });

Si el destino no se puede resolver, avisa con [WuPrefetch] hover trigger requires a target element or selector y devuelve una función no-op: nunca se hace prefetch de nada. Un selector que no coincide con nada cuenta como no resuelto.

visible — un IntersectionObserver con rootMargin: '200px', de modo que el prefetch empieza 200 px antes de que el elemento entre realmente en el viewport. Se dispara una vez y luego se desconecta.

const off = wu.prefetch('reviews', { on: 'visible', target: '#reviews-section' });

Un destino sin resolver se comporta como en hover. En un entorno sin IntersectionObserver, hace el prefetch de inmediato y devuelve una limpieza no-op.

idlerequestIdleCallback con un timeout de 3000 ms, cayendo a setTimeout(fn, 2000) donde no esté soportado. La función de limpieza cancela la callback pendiente.

const off = wu.prefetch(['profile', 'settings'], { on: 'idle' });

Solo la URL del módulo ES de la app, resuelta con la misma lógica de resolución de rutas que usa mount(). Ni el CSS, ni el manifiesto, ni ningún otro recurso. El módulo nunca se importa, ni se evalúa, ni tiene ocasión de llamar a wu.define().

Tres estrategias; gana la primera soportada:

  1. Speculation Rules API — un <script type="speculationrules"> en el head, respetando eagerness. El elemento script se reemplaza por completo en cada actualización, como exige la especificación.
  2. <link rel="modulepreload">
  3. <link rel="prefetch" as="script">

Una app se omite, en silencio o con un aviso, cuando ya se le hizo prefetch, ya está montada, ya está definida, o no está registrada ([WuPrefetch] App "<name>" not registered, cannot prefetch). Si una URL no se puede resolver, se avisa y se omite: los fallos de prefetch nunca lanzan y nunca rompen la página.

prefetchAll(options?: PrefetchOptions): Promise<void | (() => void)>

Hace prefetch de todas las apps registradas que no estén montadas y a las que no se les haya hecho ya.

// Después de que el camino crítico haya renderizado
await wu.prefetchAll({ on: 'idle' });

Caso límite: una asimetría con prefetch(). prefetchAll() es una función async, así que con un disparador diferido la función de limpieza vuelve envuelta en una Promise:

const off = wu.prefetchAll({ on: 'idle' }); // Promise<Function>, no Function
off(); // TypeError
const realOff = await wu.prefetchAll({ on: 'idle' }); // esto sí funciona
realOff();

Si no hay nada de lo que hacer prefetch, registra [WuPrefetch] No apps to prefetch y resuelve undefined.

getStats(): {
prefetched: string[];
activeObservers: number;
activeListeners: number;
speculationRulesSupported: boolean;
modulePreloadSupported: boolean;
speculationRules: object;
}
const s = wu.prefetcher.getStats();
console.log(s.prefetched); // qué se ha precalentado
console.log(s.speculationRulesSupported);

prefetched contiene una mezcla de nombres de app y URLs resueltas: es el conjunto de deduplicación, no una lista limpia. Además es la única forma de comprobar si algo tiene ya prefetch; no existe isPrefetched().

  • No hay método cancel(). La función de limpieza de cada llamada es la única forma de cancelar un prefetch diferido. Consérvala.
  • No hay isPrefetched(). Usa getStats().prefetched.includes(name), teniendo en cuenta que el conjunto mezcla nombres y URLs.
  • Solo el módulo. Las hojas de estilo, las fuentes y las imágenes que necesita la app no se precargan. En una app cargada de CSS, la ganancia es menor de lo que esperarías.
  • El tipo de retorno varía según el disparador, y una errata en el disparador lo cambia en silencio.
  • prefetchAll() envuelve su retorno en una Promise y prefetch() no.
  • Prerender no está implementado. La estructura interna de speculation rules reserva una lista prerender, pero nada escribe en ella.
  • El prefetch es una sugerencia, no una garantía. El navegador puede ignorarlo bajo presión de memoria, en una conexión con tarifa medida, o con el ahorro de datos activado.
  • Cuesta ancho de banda. Hacer prefetch de todo en una conexión móvil para ahorrar unos cientos de milisegundos suele ser el intercambio equivocado. Haz prefetch de lo que el usuario esté a punto de abrir de forma plausible.