From 02e79ea60a3605fa26ad8b4c5ba1b9b5ff872ea8 Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Thu, 1 Aug 2024 09:36:50 -0600 Subject: [PATCH 1/8] feat: :sparkles: prefetch relevant pages --- assets/global.js | 66 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/assets/global.js b/assets/global.js index ce13bf6514a..966ec98888e 100644 --- a/assets/global.js +++ b/assets/global.js @@ -214,6 +214,72 @@ function onKeyUpEscape(event) { summaryElement.focus(); } +/** + * Prefetches a page by adding a element to the document's head. + * @param {string} href - The URL of the page to prefetch. + * @param {string} [priority='auto'] - The priority of the prefetch request. Defaults to 'auto'. + */ +function addPrefetchLink(href, priority = 'auto') { + if (!href) return + if (document.querySelector(`link[rel="prefetch"][href="${href}"]`)) return + + try { + const link = document.createElement('link') + link.rel = 'prefetch' + link.href = href + link.fetchPriority = priority + link.as = 'document' + document.head.appendChild(link) + } catch (e) { + console.error('Failed to prefetch page', e) + } +} + +/** + * Prefetches pages based on the specified method. + * @param {'mouseover' | 'intersection'} method - The method to use for prefetching pages. + */ +function initPagePrefetching(method) { + if (method !== 'mouseover' && method !== 'intersection') return + console.log('prefetchPages using', method) + + const prefetchLinkRegex = /^(\/|(\/(products|collections|pages|policies)\/.*))$/ + const prefetchLinks = document.querySelectorAll('a[href]') + + const handleMouseOver = (event) => { + addPrefetchLink(event.currentTarget.href, 'high') + } + const observer = new IntersectionObserver((entries, observer) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + addPrefetchLink(entry.target.href, 'high') + observer.unobserve(entry.target) + } + }) + }) + + prefetchLinks.forEach((link) => { + if (prefetchLinkRegex.test(link.pathname)) { + if (method === 'mouseover') { + link.removeEventListener('mouseover', handleMouseOver) + link.addEventListener('mouseover', handleMouseOver) + } else if (method === 'intersection') { + observer.observe(link) + } + } + }) +} + +// If mobile, prefetch on intersection, otherwise prefetch on mouseover +const getPrefetchMethod = () => { + return window.matchMedia('(max-width: 768px)').matches + ? 'intersection' + : 'mouseover' +} +document.addEventListener('DOMContentLoaded', () => initPagePrefetching(getPrefetchMethod())) +// Update prefetch method on resize w/ debounce +window.addEventListener('resize', debounce(() => initPagePrefetching(getPrefetchMethod()), 200)) + class QuantityInput extends HTMLElement { constructor() { super(); From f9885f74b4acf4d11d4986eaf0973f538b0f04d8 Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Thu, 1 Aug 2024 10:46:37 -0600 Subject: [PATCH 2/8] fix: :mute: remove console log on prefetch init --- assets/global.js | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/global.js b/assets/global.js index 966ec98888e..8608152ba39 100644 --- a/assets/global.js +++ b/assets/global.js @@ -241,7 +241,6 @@ function addPrefetchLink(href, priority = 'auto') { */ function initPagePrefetching(method) { if (method !== 'mouseover' && method !== 'intersection') return - console.log('prefetchPages using', method) const prefetchLinkRegex = /^(\/|(\/(products|collections|pages|policies)\/.*))$/ const prefetchLinks = document.querySelectorAll('a[href]') From 9a47df93382b6a7af7183593e9e760b537188f97 Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Thu, 1 Aug 2024 12:52:12 -0600 Subject: [PATCH 3/8] feat: :sparkles: support speculation api --- assets/global.js | 11 ++++++++--- layout/theme.liquid | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/assets/global.js b/assets/global.js index 8608152ba39..ebc73c2e4f6 100644 --- a/assets/global.js +++ b/assets/global.js @@ -220,8 +220,13 @@ function onKeyUpEscape(event) { * @param {string} [priority='auto'] - The priority of the prefetch request. Defaults to 'auto'. */ function addPrefetchLink(href, priority = 'auto') { - if (!href) return - if (document.querySelector(`link[rel="prefetch"][href="${href}"]`)) return + if ( + !href || + !(new URL(href)) || + (HTMLScriptElement.supports("speculationrules")) || + window.location.href === href || + document.querySelectorAll(`link[rel="prefetch"][href="${href}"]`).length > 0 + ) return try { const link = document.createElement('link') @@ -242,7 +247,7 @@ function addPrefetchLink(href, priority = 'auto') { function initPagePrefetching(method) { if (method !== 'mouseover' && method !== 'intersection') return - const prefetchLinkRegex = /^(\/|(\/(products|collections|pages|policies)\/.*))$/ + const prefetchLinkRegex = /^(\/|(\/(products|collections|pages|blogs|policies)\/.*))$/ const prefetchLinks = document.querySelectorAll('a[href]') const handleMouseOver = (event) => { diff --git a/layout/theme.liquid b/layout/theme.liquid index 70db635823b..452900a6b84 100644 --- a/layout/theme.liquid +++ b/layout/theme.liquid @@ -372,5 +372,23 @@ {%- if settings.cart_type == 'drawer' -%} {%- endif -%} + + + From 8f7a04d565c7c0dcdc11c88d5faa87573af560c1 Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Thu, 1 Aug 2024 13:56:34 -0600 Subject: [PATCH 4/8] feat: :sparkles: unify prefetching methods --- assets/global.js | 46 ++++++++++++++++++++++++++++++++------------- layout/theme.liquid | 17 ----------------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/assets/global.js b/assets/global.js index ebc73c2e4f6..508c339a03b 100644 --- a/assets/global.js +++ b/assets/global.js @@ -220,21 +220,40 @@ function onKeyUpEscape(event) { * @param {string} [priority='auto'] - The priority of the prefetch request. Defaults to 'auto'. */ function addPrefetchLink(href, priority = 'auto') { - if ( - !href || - !(new URL(href)) || - (HTMLScriptElement.supports("speculationrules")) || - window.location.href === href || - document.querySelectorAll(`link[rel="prefetch"][href="${href}"]`).length > 0 - ) return + try { + new URL(href) // Validate URL + } catch (e) { + console.error('Invalid URL', e) + return + } + if (window.location.href === href) return + const useSpeculation = HTMLScriptElement.supports('speculationrules') try { - const link = document.createElement('link') - link.rel = 'prefetch' - link.href = href - link.fetchPriority = priority - link.as = 'document' - document.head.appendChild(link) + if (useSpeculation) { + if (document.querySelector(`script[type="speculationrules"][data-href="${href}"]`)) return + + const specRuleScript = document.createElement('script') + specRuleScript.type = 'speculationrules' + specRuleScript.dataset.href = href + const specRule = { + prefetch: [{ + urls: [href], + eagerness: 'eager' + }] + } + specRuleScript.textContent = JSON.stringify(specRule) + document.body.append(specRuleScript) + } else { + if (document.querySelector(`link[rel="prefetch"][href="${href}"]`)) return + + const link = document.createElement('link') + link.rel = 'prefetch' + link.href = href + link.fetchPriority = priority + link.as = 'document' + document.head.appendChild(link) + } } catch (e) { console.error('Failed to prefetch page', e) } @@ -280,6 +299,7 @@ const getPrefetchMethod = () => { ? 'intersection' : 'mouseover' } + document.addEventListener('DOMContentLoaded', () => initPagePrefetching(getPrefetchMethod())) // Update prefetch method on resize w/ debounce window.addEventListener('resize', debounce(() => initPagePrefetching(getPrefetchMethod()), 200)) diff --git a/layout/theme.liquid b/layout/theme.liquid index 452900a6b84..01c9e0ade3d 100644 --- a/layout/theme.liquid +++ b/layout/theme.liquid @@ -373,22 +373,5 @@ {%- endif -%} - - From 2a105c0f314d60311ef43817f98041ac849a9d7b Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Thu, 1 Aug 2024 14:05:12 -0600 Subject: [PATCH 5/8] perf: :zap: use `immediate` eagerness --- assets/global.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/global.js b/assets/global.js index 508c339a03b..8290710cd0a 100644 --- a/assets/global.js +++ b/assets/global.js @@ -239,7 +239,7 @@ function addPrefetchLink(href, priority = 'auto') { const specRule = { prefetch: [{ urls: [href], - eagerness: 'eager' + eagerness: 'immediate' }] } specRuleScript.textContent = JSON.stringify(specRule) From 324eec894fee8d9feb96c2083661776d9c40c319 Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Thu, 1 Aug 2024 14:42:58 -0600 Subject: [PATCH 6/8] refactor: :recycle: refactor wirh more links to prefetch --- assets/global.js | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/assets/global.js b/assets/global.js index 8290710cd0a..00d3603b74d 100644 --- a/assets/global.js +++ b/assets/global.js @@ -218,8 +218,9 @@ function onKeyUpEscape(event) { * Prefetches a page by adding a element to the document's head. * @param {string} href - The URL of the page to prefetch. * @param {string} [priority='auto'] - The priority of the prefetch request. Defaults to 'auto'. + * @param {'link' | 'speculation'} [method='link'] - The method to use for prefetching the page. Defaults to 'link'. */ -function addPrefetchLink(href, priority = 'auto') { +function addPrefetchLink(href, priority = 'auto', method = 'link') { try { new URL(href) // Validate URL } catch (e) { @@ -228,9 +229,8 @@ function addPrefetchLink(href, priority = 'auto') { } if (window.location.href === href) return - const useSpeculation = HTMLScriptElement.supports('speculationrules') try { - if (useSpeculation) { + if (method === 'speculation') { if (document.querySelector(`script[type="speculationrules"][data-href="${href}"]`)) return const specRuleScript = document.createElement('script') @@ -263,19 +263,26 @@ function addPrefetchLink(href, priority = 'auto') { * Prefetches pages based on the specified method. * @param {'mouseover' | 'intersection'} method - The method to use for prefetching pages. */ -function initPagePrefetching(method) { - if (method !== 'mouseover' && method !== 'intersection') return +function initPagePrefetching(deviceMethod) { + if (deviceMethod !== 'mouseover' && deviceMethod !== 'intersection') return - const prefetchLinkRegex = /^(\/|(\/(products|collections|pages|blogs|policies)\/.*))$/ + const prefetchMethod = (HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules')) + ? 'speculation' + : 'link' + + // TODO: Fall back to link prefetching if speculation prefetch fails + + const prefetchRegions = ['products', 'collections', 'pages', 'blogs', 'policies', 'search']; + const prefetchLinkRegex = new RegExp(`^(\\/|(\\/(?:${prefetchRegions.join('|')})(\\/.*)?))$`); const prefetchLinks = document.querySelectorAll('a[href]') const handleMouseOver = (event) => { - addPrefetchLink(event.currentTarget.href, 'high') + addPrefetchLink(event.currentTarget.href, 'high', prefetchMethod) } const observer = new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { - addPrefetchLink(entry.target.href, 'high') + addPrefetchLink(entry.target.href, 'high', prefetchMethod) observer.unobserve(entry.target) } }) @@ -283,10 +290,10 @@ function initPagePrefetching(method) { prefetchLinks.forEach((link) => { if (prefetchLinkRegex.test(link.pathname)) { - if (method === 'mouseover') { + if (deviceMethod === 'mouseover') { link.removeEventListener('mouseover', handleMouseOver) link.addEventListener('mouseover', handleMouseOver) - } else if (method === 'intersection') { + } else if (deviceMethod === 'intersection') { observer.observe(link) } } From e5c3c70756d47054257b08fd10c9136f74f82c42 Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Fri, 2 Aug 2024 11:48:02 -0600 Subject: [PATCH 7/8] feat(prefetch): :sparkles: disable on save data mode, add more documentation --- assets/global.js | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/assets/global.js b/assets/global.js index 00d3603b74d..439a50ecefc 100644 --- a/assets/global.js +++ b/assets/global.js @@ -276,9 +276,9 @@ function initPagePrefetching(deviceMethod) { const prefetchLinkRegex = new RegExp(`^(\\/|(\\/(?:${prefetchRegions.join('|')})(\\/.*)?))$`); const prefetchLinks = document.querySelectorAll('a[href]') - const handleMouseOver = (event) => { + const handleMouseOver = (event) => addPrefetchLink(event.currentTarget.href, 'high', prefetchMethod) - } + const observer = new IntersectionObserver((entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { @@ -300,13 +300,29 @@ function initPagePrefetching(deviceMethod) { }) } -// If mobile, prefetch on intersection, otherwise prefetch on mouseover + +/** + * Returns the preferred method for prefetching based on the window size. + * @returns {'intersection' | 'mouseover'} The preferred method for prefetching. Possible values are 'intersection' or 'mouseover'. + */ const getPrefetchMethod = () => { return window.matchMedia('(max-width: 768px)').matches ? 'intersection' : 'mouseover' } +/** + * Determine if devices should even consider prefetching + * Low power mode, data saver, etc. + * @returns {boolean} Whether or not prefetching should be used. + */ +const shouldUsePrefetching = () => { + // Data saver mode + if (navigator.connection?.saveData) return false + + // TODO: Low power mode +} + document.addEventListener('DOMContentLoaded', () => initPagePrefetching(getPrefetchMethod())) // Update prefetch method on resize w/ debounce window.addEventListener('resize', debounce(() => initPagePrefetching(getPrefetchMethod()), 200)) From d3dd79e6b0715be8a54fc1b0fcc3753b8d08359e Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Fri, 2 Aug 2024 12:00:26 -0600 Subject: [PATCH 8/8] feat(prefetch): :sparkles: disable prefetch on low battery --- assets/global.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/assets/global.js b/assets/global.js index 439a50ecefc..5774555cf5c 100644 --- a/assets/global.js +++ b/assets/global.js @@ -264,6 +264,7 @@ function addPrefetchLink(href, priority = 'auto', method = 'link') { * @param {'mouseover' | 'intersection'} method - The method to use for prefetching pages. */ function initPagePrefetching(deviceMethod) { + if (!shouldUsePrefetching()) return if (deviceMethod !== 'mouseover' && deviceMethod !== 'intersection') return const prefetchMethod = (HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules')) @@ -300,7 +301,6 @@ function initPagePrefetching(deviceMethod) { }) } - /** * Returns the preferred method for prefetching based on the window size. * @returns {'intersection' | 'mouseover'} The preferred method for prefetching. Possible values are 'intersection' or 'mouseover'. @@ -316,11 +316,17 @@ const getPrefetchMethod = () => { * Low power mode, data saver, etc. * @returns {boolean} Whether or not prefetching should be used. */ -const shouldUsePrefetching = () => { +const shouldUsePrefetching = async () => { // Data saver mode if (navigator.connection?.saveData) return false - // TODO: Low power mode + // Low power mode + // Javascript can't detect low power mode, so we'll work off actual battery level + if (navigator.getBattery) { + navigator.getBattery().then((battery) => { + if (battery.level < 0.2) return false + }) + } } document.addEventListener('DOMContentLoaded', () => initPagePrefetching(getPrefetchMethod()))