Prefetch on hover
NavLink can warm the target remote bundle on hover, focus, and touch-start so the navigation feels instant. Build a central NavLinkPrefetchProvider in the host and turn it on per link, or call the imperative prefetchRoute() from any event handler.
When does prefetch pay off?
touchstart — roughly 80ms before the actual click.Configure
Wrap your app once with the provider so every NavLink prefetch resolves through the same routes + remotes config. The provider is essentially a React context — no extra runtime cost when prefetch is off.
import { NavLink, NavLinkPrefetchProvider } from '@jorvel/runtime';
const REMOTES = {
dashboard: { name: 'dashboard', entryUrl: '/jorvel/remotes/dashboard/remoteEntry.js' },
};
const HOST_ROUTES = [
{ path: '/dashboard/*', remote: 'dashboard', module: './App' },
{ path: '/', remote: 'dashboard', module: './App' },
];
<NavLinkPrefetchProvider config={{ routes: HOST_ROUTES, remotes: REMOTES }}>
<NavLink to="/dashboard/settings" label="Settings" prefetch />
</NavLinkPrefetchProvider>Per-link override
Need a different routes/remotes map for a link (e.g. an admin destination served from a different federation graph)? Pass the prefetch config inline.
<NavLink to="/profile" label="Profile" prefetch={{
routes: HOST_ROUTES,
remotes: { profile: { name: 'profile', entryUrl: '...' } },
}} />Imperative API
Fire a prefetch from any event handler — keyboard shortcut, custom hover region, viewport-observer. The call is deduped by URL so spamming it from anIntersectionObserver is safe.
import { prefetchRoute } from '@jorvel/runtime';
await prefetchRoute('/dashboard/reports', { routes: HOST_ROUTES, remotes: REMOTES });How it works
- Resolves the URL against
routesto find the target remote name + entry URL. - Inserts a
<link rel="prefetch" as="script">for the remoteEntry. - Calls
loadRemoteEntryin the background — dedupes across the prefetch and the eventual real load, so the navigation pays only the React-render cost. - Emits
jorvel:remote-loadtelemetry with{ source: 'prefetch' }so observability dashboards can separate proactive loads from on-demand. - Respects the user's connection:
navigator.connection.saveData === trueskips prefetch. - Respects
prefers-reduced-data: returns immediately without inserting the link.
Event triggers on NavLink
prefetch on a NavLink attaches three listeners. Each fires once per link, then unsubscribes — the prefetch cache handles deduplication.
| Event | Why |
|---|---|
mouseenter | Desktop intent — typical 200-400ms before click |
focus | Keyboard navigation; also fires when a tap focuses the link on mobile |
touchstart | Mobile intent — ~80ms before the click event |
Cancelling a prefetch
Prefetches are fire-and-forget — once started, they run to completion. If you need to invalidate (e.g. after auth changes that require different headers), wipe the dedup cache:
import { resetPrefetchCache } from '@jorvel/runtime';
bus.on('auth:logout', resetPrefetchCache); // force re-fetch with new auth headersBandwidth budget
Don't prefetch everything
idle: true after first paint.Rough budget per remote (gzipped, includes remoteEntry.js + first chunk):
| Remote size | 3G data cost | Time at 1Mbps |
|---|---|---|
| 10 KB | negligible | ~80ms |
| 40 KB | tolerable on Wi-Fi | ~320ms |
| 100 KB | noticeable on metered | ~800ms |
Recipes
Prefetch the most-clicked nav link only
<nav>
<NavLink to="/" label="Home" />
<NavLink to="/dashboard" label="Dashboard" prefetch /> {/* 70% of clicks */}
<NavLink to="/billing" label="Billing" />
<NavLink to="/settings" label="Settings" />
</nav>Prefetch after auth completes
import { prefetchRoute } from '@jorvel/runtime';
import { bus } from './bus';
bus.on('auth:ready', () => {
prefetchRoute('/dashboard', { routes: HOST_ROUTES, remotes: REMOTES });
});Prefetch on scroll-into-view of a CTA
function HeroCta() {
const ref = useRef<HTMLAnchorElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
prefetchRoute('/signup', { routes: HOST_ROUTES, remotes: REMOTES });
io.disconnect();
}
});
io.observe(el);
return () => io.disconnect();
}, []);
return <a ref={ref} href="/signup">Get started</a>;
}Prefetch vs. preload vs. eager
prefetch (this page) | preload (concurrent) | Eager (sync) | |
|---|---|---|---|
| Trigger | User intent | After first paint | Initial load |
| Cost | Pay-on-hover only | One-time after FP | Blocks LCP |
| Use when | One next-click | Likely next-clicks (2-3) | Always-needed shell |
