diff options
| author | eltrac <hi@eltr.ac> | 2026-02-20 11:48:14 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-02-20 11:48:14 +0800 |
| commit | 346136ac8314ce5c8e4f06f55d642fee1e788a92 (patch) | |
| tree | c425c88cdf4b5c83cd01e573b676a4b228328c9e /assets | |
| parent | f0a89c42c21eec6bcce6c1b21dfbf4a73b178099 (diff) | |
perf: minify instantpage
Diffstat (limited to 'assets')
| -rw-r--r-- | assets/js/instantpage.js | 452 | ||||
| -rw-r--r-- | assets/js/instantpage.min.js | 2 |
2 files changed, 2 insertions, 452 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 |
