Skip to content

feat: add link prefetching support#3790

Open
bartlomieju wants to merge 1 commit into
mainfrom
feat/link-prefetching
Open

feat: add link prefetching support#3790
bartlomieju wants to merge 1 commit into
mainfrom
feat/link-prefetching

Conversation

@bartlomieju
Copy link
Copy Markdown
Contributor

Summary

  • Adds f-prefetch attribute for prefetching partial navigations ahead of time
  • Supports four strategies: hover (default), viewport, load, none
  • Container-level defaults via f-prefetch on parent elements (e.g. f-client-nav containers)
  • Integrates with the existing partials system — prefetches the partial HTML, not full pages
  • Respects navigator.connection.saveData and slow connection types
  • Deduplicates in-flight requests and caches responses with 30s TTL
  • Uses MutationObserver to handle dynamically added links

Usage

<nav f-client-nav f-prefetch="hover">
  <!-- All links prefetch on hover by default -->
  <a href="/about">About</a>
  <a href="/dashboard" f-prefetch="viewport">Dashboard</a>
  <a href="/heavy-page" f-prefetch="none">Heavy Page</a>
</nav>

Test plan

  • All existing 120 tests pass
  • Manual testing with f-prefetch attribute on links
  • Verify hover strategy triggers fetch on pointerenter
  • Verify viewport strategy triggers fetch when link scrolls into view
  • Verify load strategy triggers fetch immediately
  • Verify none strategy prevents prefetching
  • Verify container-level defaults propagate to child links
  • Verify Save-Data is respected
  • Verify cache TTL expiry works correctly
  • Verify cached response is consumed on navigation (no double fetch)

Closes #3788

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
Copy link
Copy Markdown
Contributor

@lunadogbot lunadogbot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. In-flight race in getCachedResponse: the function is sync and only checks prefetchCache. A user who clicks on a hover-prefetched link before its fetch resolves hits the inflightRequests window — prefetchCache.get(url) returns undefined, so partials.ts:fetchPartials falls through and issues a second fetch(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:363 already lives inside an async function, so await 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 stubbed IntersectionObserver.
  • nit: prefetchedUrls.add followed by prefetchCache.has(url) check at prefetch.ts:79-82 is unreachable in practice — anything in prefetchCache was added by a prior prefetch() call that also added to prefetchedUrls, so the earlier prefetchedUrls.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add link prefetching support

2 participants