Skip to content

Commit

Permalink
Use speculation rules prefetch/prerender when available
Browse files Browse the repository at this point in the history
Speculation rules prefetch, currently only available in Chromium-based browsers, has some advantages over <link rel=prefetch>:

* It stores the resources in a per-document in-memory cache, instead of in the HTTP cache, which can be slightly faster.

* Because it has its own cache, it is not disabled by HTTP caching headers like Vary or Cache-Control.

* It is automatically integrated into various user-respectful browser settings like Data Saver, Battery Saver, and memory pressure monitoring.

* It has better cross-site support than <link rel=prefetch>, including the nonstandard as=document variant, because it disables itself if the destination site has cookies, whereas <link rel=prefetch> can cache the wrong version of the document (the version without cookies).

* It shows up nicely in the DevTools speculative loads panel.

The implementation strategy is to just insert a <script type=speculationrules> element, with a single list rule pointing to the target URL, instead of inserting the corresponding <link rel=prefetch> element.

Additionally, we add the ability to configure prerendering, instead of prerendering, via the data-instant-specrules=prerender attribute. (Use of speculation rules can also be turned off, via data-instant-specrules=no.) Prerendering is more complex and risky, but can give a significant speed boost.

More information on speculation rules prefetch and prerender is available at https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API.

When speculation rules are not available, we fall back to <link rel=prefetch> as usual.
  • Loading branch information
domenic committed Feb 22, 2024
1 parent 3525715 commit c0cd06e
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 6 deletions.
43 changes: 40 additions & 3 deletions instantpage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*! instant.page v5.2.0 - (C) 2019-2023 Alexandre Dieulot - https://instant.page/license */

let _chromiumMajorVersionInUserAgent = null
, _speculationRulesType
, _allowQueryString
, _allowExternalLinks
, _useWhitelist
Expand Down Expand Up @@ -53,6 +54,16 @@ function init() {
return
}

_speculationRulesType = 'none'
if (HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules')) {
const speculationRulesConfig = document.body.dataset.instantSpecrules;
if (speculationRulesConfig == 'prerender') {
_speculationRulesType = 'prerender'
} else if (speculationRulesConfig != 'no') {
_speculationRulesType = 'prefetch'
}
}

const mousedownShortcut = 'instantMousedownShortcut' in document.body.dataset
_allowQueryString = 'instantAllowQueryString' in document.body.dataset
_allowExternalLinks = 'instantAllowExternalLinks' in document.body.dataset
Expand Down Expand Up @@ -264,7 +275,7 @@ function isPreloadable(anchorElement) {
if (anchorElement.origin != location.origin) {
let allowed = _allowExternalLinks || 'instant' in anchorElement.dataset
if (!allowed || !_chromiumMajorVersionInUserAgent) {
// Chromium-only: see comment on “restrictive prefetch”
// Chromium-only: see comment on “restrictive prefetch” and “cross-site speculation rules prefetch”
return
}
}
Expand Down Expand Up @@ -297,6 +308,34 @@ function preload(url, fetchPriority = 'auto') {
return
}

if (_speculationRulesType != 'none') {
preloadUsingSpeculationRules(url)
} else {
preloadUsingLinkElement(url, fetchPriority)
}

_preloadedList.add(url)
}

function preloadUsingSpeculationRules(url) {
const scriptElement = document.createElement('script')
scriptElement.type = 'speculationrules'

scriptElement.textContent = JSON.stringify({
[_speculationRulesType]: [{
source: 'list',
urls: [url]
}]
})

// When using speculation rules, cross-site prefetch is supported, but will
// only work if the user has no cookies for the destination site. The
// prefetch will not be sent, if the user does have such cookies.

document.head.appendChild(scriptElement)
}

function preloadUsingLinkElement(url, fetchPriority = 'auto') {
const linkElement = document.createElement('link')
linkElement.rel = 'prefetch'
linkElement.href = url
Expand Down Expand Up @@ -325,6 +364,4 @@ function preload(url, fetchPriority = 'auto') {
// event, but might be bad when prefetching every link in the viewport.

document.head.appendChild(linkElement)

_preloadedList.add(url)
}
4 changes: 3 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ function init() {
}

async function requestListener(req, res) {
const isPrefetched = req.headers['x-moz'] == 'prefetch' /* Firefox 109 */ || req.headers['purpose'] == 'prefetch' /* Chrome 110 & Safari 16.3 */
const isPrefetched = req.headers['x-moz'] == 'prefetch' /* Firefox 109 */ ||
req.headers['purpose'] == 'prefetch' /* Chrome 110 & Safari 16.3 */ ||
req.headers['sec-purpose'].startsWith('prefetch') /* Chrome 110 speculation rules */
const prefetchIndicator = isPrefetched ? 'PF' : ' F'
const type = req.headers['sec-fetch-dest'] ? req.headers['sec-fetch-dest'].toUpperCase()[0] : '.'
const spaces = ' '.repeat(Math.max(0, 16 - req.url.length))
Expand Down
1 change: 1 addition & 0 deletions test/tests/hover-long-enough-then-click/2.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
transferSize: navigationPerformanceEntry.transferSize,
deliveryType: navigationPerformanceEntry.deliveryType,
}),
})

Expand Down
2 changes: 1 addition & 1 deletion test/tests/hover-long-enough-then-click/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export const environment = {
}

export function checkExpectation(data) {
return data.transferSize === 0
return data.transferSize === 0 || data.deliveryType === 'navigational-prefetch'
}
1 change: 1 addition & 0 deletions test/tests/no-double-download/2.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
transferSize: navigationPerformanceEntry.transferSize,
deliveryType: navigationPerformanceEntry.deliveryType,
}),
})

Expand Down
2 changes: 1 addition & 1 deletion test/tests/no-double-download/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export const environment = {
}

export function checkExpectation(data) {
return data.transferSize === 0
return data.transferSize === 0 || data.deliveryType === 'navigational-prefetch'
}

0 comments on commit c0cd06e

Please sign in to comment.