feat: add link prefetching support#3790
Conversation
Adds f-prefetch attribute support with four strategies: - hover (default): prefetch on pointer enter / focus - viewport: prefetch when link enters viewport via IntersectionObserver - load: prefetch immediately on page load - none: opt out of prefetching Supports container-level defaults (e.g. f-prefetch="hover" on a nav element applies to all child links). Respects Save-Data / slow connections, deduplicates in-flight requests, and caches responses with a 30s TTL. Cached partials are consumed on navigation to avoid redundant network requests. Closes #3788
lunadogbot
left a comment
There was a problem hiding this comment.
Strategy reads well — closest() for container defaults, IntersectionObserver for viewport, MutationObserver for dynamic links, navigator.connection.saveData + slow-type opt-out, single-shot cache consumption on click.
One blocker:
-
In-flight race in
getCachedResponse: the function is sync and only checksprefetchCache. A user who clicks on ahover-prefetched link before its fetch resolves hits theinflightRequestswindow —prefetchCache.get(url)returns undefined, sopartials.ts:fetchPartialsfalls through and issues a secondfetch(partialUrl, init). Two concurrent requests for the same URL is the exact case the inflight map exists to prevent. The fix is to await the inflight promise:export async function getCachedResponse(partialUrl: URL): Promise<Response | null> { const url = partialUrl.href; const inflight = inflightRequests.get(url); if (inflight) await inflight; // ...existing TTL check + delete-and-return }
The call site at
partials.ts:363already lives inside an async function, soawait getCachedResponse(partialUrl)is a one-character change.
- nit: no tests for the new 239-line module. The eligibility rules (origin, target, fragment-only hrefs), strategy resolution (element vs. container vs. nested overrides), TTL expiry, and single-use consumption are all unit-testable with
jsdom/a stubbedIntersectionObserver. - nit:
prefetchedUrls.addfollowed byprefetchCache.has(url)check atprefetch.ts:79-82is unreachable in practice — anything inprefetchCachewas added by a priorprefetch()call that also added toprefetchedUrls, so the earlierprefetchedUrls.has(url)guard already returned. The TTL-staleness branch you intended to hit will never fire. - nit: `getStrategy` returns `"none"` when neither the element nor any ancestor has `f-prefetch`. With the current setup, that means prefetch is off by default unless opt-in — worth a brief doc note since the PR description shows it as default-hover within an `f-prefetch="hover"` container.
Summary
f-prefetchattribute for prefetching partial navigations ahead of timehover(default),viewport,load,nonef-prefetchon parent elements (e.g.f-client-navcontainers)navigator.connection.saveDataand slow connection typesMutationObserverto handle dynamically added linksUsage
Test plan
f-prefetchattribute on linksCloses #3788