summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-02-20 11:48:14 +0800
committereltrac <hi@eltr.ac>2026-02-20 11:48:14 +0800
commit346136ac8314ce5c8e4f06f55d642fee1e788a92 (patch)
treec425c88cdf4b5c83cd01e573b676a4b228328c9e
parentf0a89c42c21eec6bcce6c1b21dfbf4a73b178099 (diff)
perf: minify instantpage
-rw-r--r--assets/js/instantpage.js452
-rw-r--r--assets/js/instantpage.min.js2
-rw-r--r--layouts/_partials/scripts.html2
3 files changed, 3 insertions, 453 deletions
diff --git a/assets/js/instantpage.js b/assets/js/instantpage.js
deleted file mode 100644
index 60f1cb17..00000000
--- a/assets/js/instantpage.js
+++ /dev/null
@@ -1,452 +0,0 @@
-/*! instant.page v5.2.0 - (C) 2019-2025 Alexandre Dieulot - https://instant.page/license */
-
-let _chromiumMajorVersionInUserAgent = null
- , _speculationRulesType
- , _allowQueryString
- , _allowExternalLinks
- , _useWhitelist
- , _delayOnHover = 65
- , _lastTouchstartEvent
- , _mouseoverTimer
- , _preloadedList = new Set()
-
-init()
-
-function init() {
- const supportChecksRelList = document.createElement('link').relList
-
- const supportsPrefetch = supportChecksRelList.supports('prefetch')
- if (!supportsPrefetch) {
- return
- }
-
- const chromium100Check = 'throwIfAborted' in AbortSignal.prototype // Chromium 100+, Safari 15.4+, Firefox 97+
- const firefox115AndSafari17_0Check = supportChecksRelList.supports('modulepreload') // Firefox 115+, Safari 17.0+, Chromium 66+
- const safari15_4AndFirefox116Check = Intl.PluralRules && 'selectRange' in Intl.PluralRules.prototype // Safari 15.4+, Firefox 116+, Chromium 106+
- const firefox115AndSafari15_4Check = firefox115AndSafari17_0Check || safari15_4AndFirefox116Check
- const isBrowserSupported = chromium100Check && firefox115AndSafari15_4Check
- if (!isBrowserSupported) {
- return
- }
- // In order to lessen maintenance and unnoticed bugs we only support:
- // - Chromium ⩾ 100 — UC Browser 14
- // - Gecko as in Firefox ⩾ 115 — last version supported on Windows 7
- // - WebKit as in Safari ⩾ 15.4 — last major WebKit version supported on iPhone 6s & 7
- //
- // WebKit doesn’t support prefetch anyway, but instant.page might
- // eventually drop this requirement by providing an option for
- // fetch()-based preloading.
- //
- // Additionally, instant.page should not cause JavaScript errors in:
- // - Chromium ⩾ 61
- // - Gecko as in Firefox ⩾ 60
- // - WebKit as in Safari ⩾ 10.1 (iOS ⩾ 10.3 and macOS ⩾ 10.10)
- // Browser engines older than that don’t support <script type=module>
- // and thus don’t load instant.page at all.
-
- const handleVaryAcceptHeader = 'instantVaryAccept' in document.body.dataset || 'Shopify' in window
- // The `Vary: Accept` header when received in Chromium 79–109 makes prefetches
- // unusable, as Chromium used to send a different `Accept` header.
- // It’s applied on all Shopify sites by default, as Shopify is very popular
- // and is the main source of this problem.
- // `window.Shopify` only exists on “classic” Shopify sites. Those using
- // Hydrogen (Remix SPA) aren’t concerned.
-
- const chromiumUserAgentIndex = navigator.userAgent.indexOf('Chrome/')
- if (chromiumUserAgentIndex > -1) {
- _chromiumMajorVersionInUserAgent = parseInt(navigator.userAgent.substring(chromiumUserAgentIndex + 'Chrome/'.length))
- }
- // The user agent client hints API is a theoretically more reliable way to
- // get Chromium’s version… but it’s not available in Samsung Internet 20.
- // It also requires a secure context, which would make debugging harder,
- // and is only available in recent Chromium versions.
- // In practice, Chromium browsers never shy from announcing "Chrome" in
- // their regular user agent string, as that maximizes their compatibility.
-
- if (handleVaryAcceptHeader && _chromiumMajorVersionInUserAgent && _chromiumMajorVersionInUserAgent < 110) {
- 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 useMousedownShortcut = 'instantMousedownShortcut' in document.body.dataset
- _allowQueryString = 'instantAllowQueryString' in document.body.dataset
- _allowExternalLinks = 'instantAllowExternalLinks' in document.body.dataset
- _useWhitelist = 'instantWhitelist' in document.body.dataset
-
- let preloadOnMousedown = false
- let preloadOnlyOnMousedown = false
- let preloadWhenVisible = false
- if ('instantIntensity' in document.body.dataset) {
- const intensityParameter = document.body.dataset.instantIntensity
-
- if (intensityParameter == 'mousedown' && !useMousedownShortcut) {
- preloadOnMousedown = true
- }
-
- if (intensityParameter == 'mousedown-only' && !useMousedownShortcut) {
- preloadOnMousedown = true
- preloadOnlyOnMousedown = true
- }
-
- if (intensityParameter == 'viewport') {
- const isOnSmallScreen = document.documentElement.clientWidth * document.documentElement.clientHeight < 450000
- // Smartphones are the most likely to have a slow connection, and
- // their small screen size limits the number of links (and thus
- // server load).
- //
- // Foldable phones (being expensive as of 2023), tablets and PCs
- // generally have a decent connection, and a big screen displaying
- // more links that would put more load on the server.
- //
- // iPhone 14 Pro Max (want): 430×932 = 400 760
- // Samsung Galaxy S22 Ultra with display size set to 80% (want):
- // 450×965 = 434 250
- // Small tablet (don’t want): 600×960 = 576 000
- // Those number are virtual screen size, the viewport (used for
- // the check above) will be smaller with the browser’s interface.
-
- const isNavigatorConnectionSaveDataEnabled = navigator.connection && navigator.connection.saveData
- const isNavigatorConnectionLike2g = navigator.connection && navigator.connection.effectiveType && navigator.connection.effectiveType.includes('2g')
- const isNavigatorConnectionAdequate = !isNavigatorConnectionSaveDataEnabled && !isNavigatorConnectionLike2g
-
- if (isOnSmallScreen && isNavigatorConnectionAdequate) {
- preloadWhenVisible = true
- }
- }
-
- if (intensityParameter == 'viewport-all') {
- preloadWhenVisible = true
- }
-
- const intensityAsInteger = parseInt(intensityParameter)
- if (!isNaN(intensityAsInteger)) {
- _delayOnHover = intensityAsInteger
- }
- }
-
- const eventListenersOptions = {
- capture: true,
- passive: true,
- }
-
- if (preloadOnlyOnMousedown) {
- document.addEventListener('touchstart', touchstartEmptyListener, eventListenersOptions)
- }
- else {
- document.addEventListener('touchstart', touchstartListener, eventListenersOptions)
- }
-
- if (!preloadOnMousedown) {
- document.addEventListener('mouseover', mouseoverListener, eventListenersOptions)
- }
-
- if (preloadOnMousedown) {
- document.addEventListener('mousedown', mousedownListener, eventListenersOptions)
- }
- if (useMousedownShortcut) {
- document.addEventListener('mousedown', mousedownShortcutListener, eventListenersOptions)
- }
-
- if (preloadWhenVisible) {
- let requestIdleCallbackOrFallback = window.requestIdleCallback
- // Safari has no support as of 16.3: https://webkit.org/b/164193
- if (!requestIdleCallbackOrFallback) {
- requestIdleCallbackOrFallback = (callback) => {
- callback()
- // A smarter fallback like setTimeout is not used because devices that
- // may eventually be eligible to a Safari version supporting prefetch
- // will be very powerful.
- // The weakest devices that could be eligible are the 2017 iPad and
- // the 2016 MacBook.
- }
- }
-
- requestIdleCallbackOrFallback(function observeIntersection() {
- const intersectionObserver = new IntersectionObserver((entries) => {
- entries.forEach((entry) => {
- if (entry.isIntersecting) {
- const anchorElement = entry.target
- intersectionObserver.unobserve(anchorElement)
- preload(anchorElement.href)
- }
- })
- })
-
- document.querySelectorAll('a').forEach((anchorElement) => {
- if (isPreloadable(anchorElement)) {
- intersectionObserver.observe(anchorElement)
- }
- })
- }, {
- timeout: 1500,
- })
- }
-}
-
-function touchstartListener(event) {
- _lastTouchstartEvent = event
-
- const anchorElement = event.target.closest('a')
-
- if (!isPreloadable(anchorElement)) {
- return
- }
-
- preload(anchorElement.href, 'high')
-}
-
-function touchstartEmptyListener(event) {
- _lastTouchstartEvent = event
-}
-
-function mouseoverListener(event) {
- if (isEventLikelyTriggeredByTouch(event)) {
- // This avoids uselessly adding a mouseout event listener and setting a timer.
- return
- }
-
- if (!('closest' in event.target)) {
- return
- // Without this check sometimes an error “event.target.closest is not a function” is thrown, for unknown reasons
- // That error denotes that `event.target` isn’t undefined. My best guess is that it’s the Document.
- //
- // Details could be gleaned from throwing such an error:
- //throw new TypeError(`instant.page non-element event target: timeStamp=${~~event.timeStamp}, type=${event.type}, typeof=${typeof event.target}, nodeType=${event.target.nodeType}, nodeName=${event.target.nodeName}, viewport=${innerWidth}x${innerHeight}, coords=${event.clientX}x${event.clientY}, scroll=${scrollX}x${scrollY}`)
- }
- const anchorElement = event.target.closest('a')
-
- if (!isPreloadable(anchorElement)) {
- return
- }
-
- anchorElement.addEventListener('mouseout', mouseoutListener, {passive: true})
-
- _mouseoverTimer = setTimeout(() => {
- preload(anchorElement.href, 'high')
- _mouseoverTimer = null
- }, _delayOnHover)
-}
-
-function mousedownListener(event) {
- if (isEventLikelyTriggeredByTouch(event)) {
- // When preloading only on mousedown, not touch, we need to stop there
- // because touches send compatibility mouse events including mousedown.
- //
- // (When preloading on touchstart, instructions below this block would
- // have no effect.)
- return
- }
-
- const anchorElement = event.target.closest('a')
-
- if (!isPreloadable(anchorElement)) {
- return
- }
-
- preload(anchorElement.href, 'high')
-}
-
-function mouseoutListener(event) {
- if (event.relatedTarget && event.target.closest('a') == event.relatedTarget.closest('a')) {
- return
- }
-
- if (_mouseoverTimer) {
- clearTimeout(_mouseoverTimer)
- _mouseoverTimer = null
- }
-}
-
-function mousedownShortcutListener(event) {
- if (isEventLikelyTriggeredByTouch(event)) {
- // Due to a high potential for complications with this mousedown shortcut
- // combined with other parties’ JavaScript code, we don’t want it to run
- // at all on touch devices, even though mousedown and click are triggered
- // at almost the same time on touch.
- return
- }
-
- const anchorElement = event.target.closest('a')
-
- if (event.which > 1 || event.metaKey || event.ctrlKey) {
- return
- }
-
- if (!anchorElement) {
- return
- }
-
- anchorElement.addEventListener('click', function (event) {
- if (event.detail == 1337) {
- return
- }
-
- event.preventDefault()
- }, {capture: true, passive: false, once: true})
-
- const customEvent = new MouseEvent('click', {view: window, bubbles: true, cancelable: false, detail: 1337})
- anchorElement.dispatchEvent(customEvent)
-}
-
-function isEventLikelyTriggeredByTouch(event) {
- // Touch devices fire “mouseover” and “mousedown” (and other) events after
- // a touch for compatibility reasons.
- // This function checks if it’s likely that we’re dealing with such an event.
-
- if (!_lastTouchstartEvent || !event) {
- return false
- }
-
- if (event.target != _lastTouchstartEvent.target) {
- return false
- }
-
- const now = event.timeStamp
- // Chromium (tested Chrome 95 and 122 on Android) sometimes uses the same
- // event.timeStamp value in touchstart, mouseover, and mousedown.
- // Testable in test/extras/delay-not-considered-touch.html
- // This is okay for our purpose: two equivalent timestamps will be less
- // than the max duration, which means they’re related events.
- // TODO: fill/find Chromium bug
- const durationBetweenLastTouchstartAndNow = now - _lastTouchstartEvent.timeStamp
-
- const MAX_DURATION_TO_BE_CONSIDERED_TRIGGERED_BY_TOUCHSTART = 2500
- // How long after a touchstart event can a simulated mouseover/mousedown event fire?
- // /test/extras/delay-not-considered-touch.html tries to answer that question.
- // I saw up to 1450 ms on an overwhelmed Samsung Galaxy S2.
- // On the other hand, how soon can an unrelated mouseover event happen after an unrelated touchstart?
- // Meaning the user taps a link, then grabs their pointing device and clicks another/the same link.
- // That scenario could occur if a user taps a link, thinks it hasn’t worked, and thus fall back to their pointing device.
- // I do that in about 1200 ms on a Chromebook. In which case this function returns a false positive.
- // False positives are okay, as this function is only used to decide to abort handling mouseover/mousedown/mousedownShortcut.
- // False negatives could lead to unforeseen state, particularly in mousedownShortcutListener.
-
- return durationBetweenLastTouchstartAndNow < MAX_DURATION_TO_BE_CONSIDERED_TRIGGERED_BY_TOUCHSTART
-
- // TODO: Investigate if pointer events could be used.
- // https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/pointerType
-
- // TODO: Investigate if InputDeviceCapabilities could be used to make it
- // less hacky on Chromium browsers.
- // https://developer.mozilla.org/en-US/docs/Web/API/InputDeviceCapabilities_API
- // https://wicg.github.io/input-device-capabilities/
- // Needs careful reading of the spec and tests (notably, what happens with a
- // mouse connected to an Android or iOS smartphone?) to make sure it’s solid.
- // Also need to judge if WebKit could implement it differently, as they
- // don’t mind doing when a spec gives room to interpretation.
- // It seems to work well on Chrome on ChromeOS.
-
- // TODO: Consider using event screen position as another heuristic.
-}
-
-function isPreloadable(anchorElement) {
- if (!anchorElement || !anchorElement.href) {
- return
- }
-
- if (_useWhitelist && !('instant' in anchorElement.dataset)) {
- return
- }
-
- if (anchorElement.origin != location.origin) {
- let allowed = _allowExternalLinks || 'instant' in anchorElement.dataset
- if (!allowed || !_chromiumMajorVersionInUserAgent) {
- // Chromium-only: see comment on “restrictive prefetch” and “cross-site speculation rules prefetch”
- return
- }
- }
-
- if (!['http:', 'https:'].includes(anchorElement.protocol)) {
- return
- }
-
- if (anchorElement.protocol == 'http:' && location.protocol == 'https:') {
- return
- }
-
- if (!_allowQueryString && anchorElement.search && !('instant' in anchorElement.dataset)) {
- return
- }
-
- if (anchorElement.hash && anchorElement.pathname + anchorElement.search == location.pathname + location.search) {
- return
- }
-
- if ('noInstant' in anchorElement.dataset) {
- return
- }
-
- return true
-}
-
-function preload(url, fetchPriority = 'auto') {
- if (_preloadedList.has(url)) {
- 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
-
- linkElement.fetchPriority = fetchPriority
- // By default, a prefetch is loaded with a low priority.
- // When there’s a fair chance that this prefetch is going to be used in the
- // near term (= after a touch/mouse event), giving it a high priority helps
- // make the page load faster in case there are other resources loading.
- // Prioritizing it implicitly means deprioritizing every other resource
- // that’s loading on the page. Due to HTML documents usually being much
- // smaller than other resources (notably images and JavaScript), and
- // prefetches happening once the initial page is sufficiently loaded,
- // this theft of bandwidth should rarely be detrimental.
-
- linkElement.as = 'document'
- // as=document is Chromium-only and allows cross-origin prefetches to be
- // usable for navigation. They call it “restrictive prefetch” and intend
- // to remove it: https://crbug.com/1352371
- //
- // This document from the Chrome team dated 2022-08-10
- // https://docs.google.com/document/d/1x232KJUIwIf-k08vpNfV85sVCRHkAxldfuIA5KOqi6M
- // claims (I haven’t tested) that data- and battery-saver modes as well as
- // the setting to disable preloading do not disable restrictive prefetch,
- // unlike regular prefetch. That’s good for prefetching on a touch/mouse
- // event, but might be bad when prefetching every link in the viewport.
-
- document.head.appendChild(linkElement)
-}
diff --git a/assets/js/instantpage.min.js b/assets/js/instantpage.min.js
new file mode 100644
index 00000000..761f9d34
--- /dev/null
+++ b/assets/js/instantpage.min.js
@@ -0,0 +1,2 @@
+/*! instant.page v5.2.0 - (C) 2019-2025 Alexandre Dieulot - https://instant.page/license */
+let t,e,n,o,i,r,a=null,s=65,c=new Set;function u(t){i=t;const e=t.target.closest("a");g(e)&&v(e.href,"high")}function d(t){i=t}function l(t){if(h(t))return;if(!("closest"in t.target))return;const e=t.target.closest("a");g(e)&&(e.addEventListener("mouseout",p,{passive:!0}),r=setTimeout(()=>{v(e.href,"high"),r=null},s))}function f(t){if(h(t))return;const e=t.target.closest("a");g(e)&&v(e.href,"high")}function p(t){t.relatedTarget&&t.target.closest("a")==t.relatedTarget.closest("a")||r&&(clearTimeout(r),r=null)}function m(t){if(h(t))return;const e=t.target.closest("a");if(t.which>1||t.metaKey||t.ctrlKey)return;if(!e)return;e.addEventListener("click",function(t){1337!=t.detail&&t.preventDefault()},{capture:!0,passive:!1,once:!0});const n=new MouseEvent("click",{view:window,bubbles:!0,cancelable:!1,detail:1337});e.dispatchEvent(n)}function h(t){if(!i||!t)return!1;if(t.target!=i.target)return!1;return t.timeStamp-i.timeStamp<2500}function g(t){if(t&&t.href&&(!o||"instant"in t.dataset)){if(t.origin!=location.origin){if(!(n||"instant"in t.dataset)||!a)return}if(["http:","https:"].includes(t.protocol)&&("http:"!=t.protocol||"https:"!=location.protocol)&&(e||!t.search||"instant"in t.dataset)&&!(t.hash&&t.pathname+t.search==location.pathname+location.search||"noInstant"in t.dataset))return!0}}function v(e,n="auto"){c.has(e)||("none"!=t?function(e){const n=document.createElement("script");n.type="speculationrules",n.textContent=JSON.stringify({[t]:[{source:"list",urls:[e]}]}),document.head.appendChild(n)}(e):function(t,e="auto"){const n=document.createElement("link");n.rel="prefetch",n.href=t,n.fetchPriority=e,n.as="document",document.head.appendChild(n)}(e,n),c.add(e))}!function(){const i=document.createElement("link").relList;if(!i.supports("prefetch"))return;const r="throwIfAborted"in AbortSignal.prototype,c=i.supports("modulepreload"),p=Intl.PluralRules&&"selectRange"in Intl.PluralRules.prototype;if(!r||!c&&!p)return;const h="instantVaryAccept"in document.body.dataset||"Shopify"in window,y=navigator.userAgent.indexOf("Chrome/");y>-1&&(a=parseInt(navigator.userAgent.substring(y+"Chrome/".length)));if(h&&a&&a<110)return;if(t="none",HTMLScriptElement.supports&&HTMLScriptElement.supports("speculationrules")){const e=document.body.dataset.instantSpecrules;"prerender"==e?t="prerender":"no"!=e&&(t="prefetch")}const w="instantMousedownShortcut"in document.body.dataset;e="instantAllowQueryString"in document.body.dataset,n="instantAllowExternalLinks"in document.body.dataset,o="instantWhitelist"in document.body.dataset;let b=!1,E=!1,S=!1;if("instantIntensity"in document.body.dataset){const t=document.body.dataset.instantIntensity;if("mousedown"!=t||w||(b=!0),"mousedown-only"!=t||w||(b=!0,E=!0),"viewport"==t){const t=document.documentElement.clientWidth*document.documentElement.clientHeight<45e4,e=navigator.connection&&navigator.connection.saveData,n=navigator.connection&&navigator.connection.effectiveType&&navigator.connection.effectiveType.includes("2g"),o=!e&&!n;t&&o&&(S=!0)}"viewport-all"==t&&(S=!0);const e=parseInt(t);isNaN(e)||(s=e)}const I={capture:!0,passive:!0};E?document.addEventListener("touchstart",d,I):document.addEventListener("touchstart",u,I);b||document.addEventListener("mouseover",l,I);b&&document.addEventListener("mousedown",f,I);w&&document.addEventListener("mousedown",m,I);if(S){let t=window.requestIdleCallback;t||(t=(t=>{t()})),t(function(){const t=new IntersectionObserver(e=>{e.forEach(e=>{if(e.isIntersecting){const n=e.target;t.unobserve(n),v(n.href)}})});document.querySelectorAll("a").forEach(e=>{g(e)&&t.observe(e)})},{timeout:1500})}}(); \ No newline at end of file
diff --git a/layouts/_partials/scripts.html b/layouts/_partials/scripts.html
index f7e9c36b..8e65c873 100644
--- a/layouts/_partials/scripts.html
+++ b/layouts/_partials/scripts.html
@@ -10,7 +10,7 @@ document.addEventListener("DOMContentLoaded", () => {
<!-- global JS -->
{{ $commonJS := slice
(resources.Get "js/tooltip.js")
- (resources.Get "js/instantpage.js")
+ (resources.Get "js/instantpage.min.js")
}}
{{ $commonBundle := $commonJS | resources.Concat "js/common-bundle.js" | minify | fingerprint }}
<script src="{{ $commonBundle.RelPermalink }}" integrity="{{ $commonBundle.Data.Integrity }}" defer></script>