Skip to content

Instantly share code, notes, and snippets.

@vitobotta
Last active December 20, 2023 13:34
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save vitobotta/8ac3c6f65633b5edb2949aeff0dec69b to your computer and use it in GitHub Desktop.
Save vitobotta/8ac3c6f65633b5edb2949aeff0dec69b to your computer and use it in GitHub Desktop.
// This code is to be used with https://turbo.hotwire.dev. By default Turbo keeps visited pages in its cache
// so that when you visit one of those pages again, Turbo will fetch the copy from cache first and present that to the user, then
// it will fetch the updated page from the server and replace the preview. This makes for a much more responsive navigation
// between pages. We can improve this further with the code in this file. It enables automatic prefetching of a page when you
// hover with the mouse on a link or touch it on a mobile device. There is a delay between the mouseover event and the click
// event, so with this trick the page is already being fetched before the click happens, speeding up also the first
// view of a page not yet in cache. When the page has been prefetched it is then added to Turbo's cache so it's available for
// the next visit during the same session. Turbo's default behavior plus this trick make for much more responsive UIs (non SPA).
let lastTouchTimestamp
let delayOnHover = 65
let mouseoverTimer
const pendingPrefetches = new Set()
const eventListenersOptions = {
capture: true,
passive: true,
}
class Snapshot extends Turbo.navigator.view.snapshot.constructor {
}
document.addEventListener('touchstart', touchstartListener, eventListenersOptions)
document.addEventListener('mouseover', mouseoverListener, eventListenersOptions)
function touchstartListener(event) {
if (window.disablePrefetch) {
return
}
/* Chrome on Android calls mouseover before touchcancel so `lastTouchTimestamp`
* must be assigned on touchstart to be measured on mouseover. */
lastTouchTimestamp = performance.now()
const linkElement = event.target.closest('a')
if (!isPreloadable(linkElement)) {
return
}
preload(linkElement)
}
function mouseoverListener(event) {
if (window.disablePrefetch) {
return
}
if (performance.now() - lastTouchTimestamp < 1111) {
return
}
const linkElement = event.target.closest('a')
if (!isPreloadable(linkElement)) {
return
}
const url = linkElement.getAttribute("href")
const loc = new URL(url, location.protocol + "//" + location.host).toString()
const absoluteUrl = loc.toString()
if (pendingPrefetches.has(absoluteUrl)) {
return
}
pendingPrefetches.add(absoluteUrl)
linkElement.addEventListener('mouseout', mouseoutListener, {passive: true})
mouseoverTimer = setTimeout(() => {
preload(linkElement)
mouseoverTimer = undefined
}, delayOnHover)
}
function mouseoutListener(event) {
if (event.relatedTarget && event.target.closest('a') == event.relatedTarget.closest('a')) {
return
}
if (mouseoverTimer) {
clearTimeout(mouseoverTimer)
mouseoverTimer = undefined
}
}
function isPreloadable(linkElement) {
if (!linkElement || !linkElement.getAttribute("href") || linkElement.dataset.turbo == "false" || linkElement.dataset.prefetch == "false") {
return
}
if (linkElement.origin != location.origin) {
return
}
if (!['http:', 'https:'].includes(linkElement.protocol)) {
return
}
if (linkElement.protocol == 'http:' && location.protocol == 'https:') {
return
}
if (linkElement.search && !('prefetch' in linkElement.dataset)) {
return
}
if (linkElement.hash && linkElement.pathname + linkElement.search == location.pathname + location.search) {
return
}
return true
}
function fetchPage (url, success) {
const xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.setRequestHeader('VND.PREFETCH', 'true')
xhr.setRequestHeader('Accept', 'text/html')
xhr.onreadystatechange = () => {
if (xhr.readyState !== XMLHttpRequest.DONE) return
if (xhr.status !== 200) return
success(xhr.responseText)
}
xhr.send()
}
function preload(link) {
const url = link.getAttribute("href")
const loc = new URL(url, location.protocol + "//" + location.host)
const absoluteUrl = loc.toString()
if (link.dataset.prefetchWithLink == "true") {
const prefetcher = document.createElement('link')
prefetcher.rel = 'prefetch'
prefetcher.href = url
document.head.appendChild(prefetcher)
pendingPrefetches.delete(absoluteUrl)
} else if (!Turbo.navigator.view.snapshotCache.has(loc)) {
fetchPage(url, responseText => {
const snapshot = Snapshot.fromHTMLString(responseText)
Turbo.navigator.view.snapshotCache.put(loc, snapshot)
pendingPrefetches.delete(absoluteUrl)
})
}
}
@kwhandy
Copy link

kwhandy commented Jan 30, 2022

so, how to use this?

@leouofa
Copy link

leouofa commented Jan 31, 2022

@kwhandy I use a version of this (via stimulus) to preload pages the users are likely to click on before they click it. Makes everything a lot faster.

@kylesloper
Copy link

@leouofa Mind sharing the updated version of this?

@leouofa
Copy link

leouofa commented Aug 31, 2022

@kylesloper Sure, here's the new gist.

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