fetch on click vs. fetch on intent
A normal <Link> is "click then fetch" — the user clicks, the router runs the
route's beforeLoad → loader, downloads the JS chunk, THEN paints. Preloading
inverts the timeline: the router starts that work the instant a link signals intent
(hover / tap / enters viewport). By the time the click lands, the result is already in the cache and the
navigation is INSTANT. Same data, same loaders — just moved earlier.
| piece | role | default |
|---|---|---|
preload |
trigger policy on <Link>: false | 'intent' | 'viewport' |
false (opt-in) |
'intent' |
preload on mouseenter (desktop hover) + pointerdown/touchstart (mobile tap), after preloadDelay ms |
delay 0ms |
'viewport' |
preload via IntersectionObserver the moment the link scrolls into view |
eager — all visible links |
staleTime |
how long a preloaded entry is considered fresh (no refetch on navigate) | 30_000ms (navigations: 0) |
gcTime |
how long an unused preloaded entry survives in memory before garbage collection | 300_000ms (5min) |
Deduplication is free: two links to the same route share one in-flight preload; if a preload is mid-flight when the click arrives, the result is reused — no double fetch.
1 · the preloading pipeline (edit me)
2 · Babel compiles JSX → element tree
<NavigationDemo/> becomes React.createElement(NavigationDemo).
Hovering a link calls onIntent(path,'hover') → if strategy matches, startPreload
dedups against cache[path], kicks the setTimeout "fetch", and the log + cache
snapshot re-render. A later click reads the same cache[path] — hit → INSTANT.
// (hit "compile & render" to see Babel's output)
3 · live React (the preloading pipeline, proven)
Hover a link to fire intent (with hover selected), then click it — the navigation is
INSTANT because the cache already holds the result. Switch to none, hover a fresh link —
nothing preloads; clicking it shows LOADING. The gold-check runs this whole
sequence automatically and asserts every step.
live: — · gold: hover Users → preload → click INSTANT · none → hover Settings (no preload) → click LOADING
trigger → dedup → cache → navigate
| scenario | what happens | why |
|---|---|---|
preload="intent" + hover |
mouseenter → after preloadDelay → preloadRoute() |
hover ≈ "I'm about to click" — cheapest signal with real intent |
preload="intent" + tap |
pointerdown/touchstart → preload starts (same code path as hover) | mobile has no hover; intent covers both event types |
preload="viewport" |
IntersectionObserver fires → preload every link in view | eager — best for landing pages, costs bandwidth on link farms |
| two links, same route | only the first triggers a fetch; second logs CACHE HIT | dedup key = route path; in-flight promise is shared |
| click while preloading | navigation awaits the in-flight preload → reuses result | no double fetch; click never cancels the preload |
click after staleTime |
entry is stale → refetch in background (or block, per staleReloadMode) |
freshness window balances latency vs. data currency |
| link leaves viewport, unused | after gcTime (default 5min) the entry is garbage-collected |
preloads don't leak memory forever |