Skip to content

Add opt-in Turbo Frame prefetching on hover - #30

Open
manuelpuyol wants to merge 2 commits into
github:mainfrom
manuelpuyol:mp/opt-in-frame-prefetch
Open

Add opt-in Turbo Frame prefetching on hover#30
manuelpuyol wants to merge 2 commits into
github:mainfrom
manuelpuyol:mp/opt-in-frame-prefetch

Conversation

@manuelpuyol

@manuelpuyol manuelpuyol commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add per-link, opt-in hover prefetching for Turbo navigation, including links that target Turbo Frames.

<a href="/pulls" data-turbo-prefetch>Pull requests</a>
<a href="/pulls" data-turbo-prefetch data-turbo-prefetch-delay="250">Pull requests</a>

Unmarked links do not create a timer, cache entry, lifecycle event, or additional network request. Prefetching defaults to a 100 ms hover delay; data-turbo-prefetch-delay overrides it for an individual link.

Implementation

  • Resolve data-turbo-frame, the closest frame target, or the closest frame ID and send the matching Turbo-Frame header. _top remains a full-page request.
  • Reuse the in-flight or completed prefetch response when navigation begins, avoiding a duplicate request.
  • Keep one short-lived prefetch entry keyed by URL and resolved frame target, with a 10-second default TTL.
  • Cancel pending work when hover ends and invalidate started prefetches on replacement, mutation, navigation, expiry, request failure, session stop, or page unload.
  • Exclude cross-origin, non-HTTP, same-page, download, explicitly targeted, unsafe-method, Turbo Stream, confirmation, UJS, and data-turbo="false" links.
  • Preserve turbo:before-prefetch as a cancelable application hook.

Observability

Dispatch correlated lifecycle events so consumers can calculate effectiveness:

  • turbo:prefetch-start
  • turbo:prefetch-hit
  • turbo:prefetch-waste

Each event includes an ID, URL, resolved frame, and configured delay. Hit and waste events include duration; waste events also include a reason. Hit rate is hits / starts, and waste rate is wastes / starts. A hover canceled before its delay is excluded because no request started.

Existing GitHub patches

The first commit ports the current github-ui patch-package behavior into source so a release from this branch can replace that downstream patch. It preserves frame fetch responses in render hooks, React-managed head elements, compositor-driven progress values with a standalone CSS fallback, and animation-frame-coalesced scroll reads.

Validation

  • yarn build
  • yarn lint
  • Focused Chromium prefetch suite: 6 passed
  • Full Chromium functional suite: 327 passed; one existing UJS test passed on retry and was reported as flaky

The legacy Intern runner could not execute in this container because its configured Firefox/ChromeDriver environment was unavailable; it ran zero tests.

Copilot AI balanced review requested due to automatic review settings August 14, 2026 20:44
@manuelpuyol
manuelpuyol requested a review from a team as a code owner August 14, 2026 20:44
@manuelpuyol
manuelpuyol requested a review from jibrang August 14, 2026 20:44
@manuelpuyol manuelpuyol changed the title Mp/opt in frame prefetch Add opt-in Turbo Frame prefetching on hover Aug 14, 2026
@manuelpuyol
manuelpuyol changed the base branch from mp/opt-in-frame-prefetch to main August 14, 2026 20:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds opt-in hover prefetching with frame-aware caching, lifecycle events, metrics, documentation, and functional coverage.

Changes:

  • Adds delayed per-link prefetching and response reuse.
  • Exposes prefetch lifecycle events and waste reasons.
  • Adjusts head rendering, progress-bar updates, and scroll handling.
Show a summary per file
File Description
README.md Documents hover prefetching.
src/core/drive/form_submission.ts Clears prefetches on mutations.
src/core/drive/page_renderer.ts Preserves React-managed head elements.
src/core/drive/prefetch_cache.ts Implements the prefetch cache.
src/core/drive/progress_bar.ts Uses a CSS scale variable.
src/core/frames/frame_controller.ts Exposes frame fetch responses during rendering.
src/core/index.ts Exports prefetch event types.
src/core/session.ts Integrates the prefetch observer.
src/core/view.ts Adds render fetch-response context.
src/http/fetch_request.ts Supports prefetched response reuse.
src/observers/link_prefetch_observer.ts Implements hover prefetching.
src/observers/scroll_observer.ts Throttles scroll updates.
src/tests/fixtures/hover_to_prefetch.html Provides prefetch test links.
src/tests/fixtures/test.js Logs prefetch events.
src/tests/functional/link_prefetch_observer_tests.ts Tests prefetch behavior and metrics.
src/tests/unit/export_tests.ts Verifies exported prefetch types.

Review details

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

  • Files reviewed: 16/16 changed files
  • Comments generated: 5
  • Review effort level: Balanced

Comment thread src/http/fetch_request.ts
this.delegate.requestStarted(this)
const response = await fetch(this.url.href, fetchOptions)

this.response = event.detail.fetchRequest?.response || fetch(this.url.href, fetchOptions)
const fetchEvent = event as TurboBeforeFetchRequestEvent
const { fetchOptions, url } = fetchEvent.detail

if (fetchEvent.target instanceof HTMLFormElement || fetchOptions.method !== "GET") return
Comment on lines +213 to +216
const frame = link.closest("turbo-frame")
const target = link.getAttribute("data-turbo-frame") || frame?.getAttribute("target") || frame?.id

return target && target !== "_top" ? target : null
Comment on lines +35 to +38
requestAnimationFrame(() => {
this.scrollScheduled = false
this.updatePosition({ x: window.pageXOffset, y: window.pageYOffset })
})

removeCurrentHeadProvisionalElements() {
for (const element of this.currentHeadProvisionalElements) {
if (isManagedByReact(element)) continue
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.

2 participants