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

@manuelpuyolmanuelpuyol 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.

<ahref="/pulls" data-turbo-prefetch>Pull requests</a><ahref="/pulls" data-turbo-prefetchdata-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.

CopilotAI balanced review requested due to automatic review settings August 14, 2026 20:44
@manuelpuyol
manuelpuyol requested a review from a team as a code ownerAugust 14, 2026 20:44
@manuelpuyolmanuelpuyol changed the title Mp/opt in frame prefetchAdd opt-in Turbo Frame prefetching on hoverAug 14, 2026
@manuelpuyol
manuelpuyol changed the base branch from mp/opt-in-frame-prefetch to mainAugust 14, 2026 20:47

CopilotAI 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
FileDescription
README.mdDocuments hover prefetching.
src/core/drive/form_submission.tsClears prefetches on mutations.
src/core/drive/page_renderer.tsPreserves React-managed head elements.
src/core/drive/prefetch_cache.tsImplements the prefetch cache.
src/core/drive/progress_bar.tsUses a CSS scale variable.
src/core/frames/frame_controller.tsExposes frame fetch responses during rendering.
src/core/index.tsExports prefetch event types.
src/core/session.tsIntegrates the prefetch observer.
src/core/view.tsAdds render fetch-response context.
src/http/fetch_request.tsSupports prefetched response reuse.
src/observers/link_prefetch_observer.tsImplements hover prefetching.
src/observers/scroll_observer.tsThrottles scroll updates.
src/tests/fixtures/hover_to_prefetch.htmlProvides prefetch test links.
src/tests/fixtures/test.jsLogs prefetch events.
src/tests/functional/link_prefetch_observer_tests.tsTests prefetch behavior and metrics.
src/tests/unit/export_tests.tsVerifies 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

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 freeto 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.

3 participants

@manuelpuyol@arelia