diff options
Diffstat (limited to 'packages/integrations/prefetch/src/client.ts')
-rw-r--r-- | packages/integrations/prefetch/src/client.ts | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/packages/integrations/prefetch/src/client.ts b/packages/integrations/prefetch/src/client.ts new file mode 100644 index 000000000..46a688f9d --- /dev/null +++ b/packages/integrations/prefetch/src/client.ts @@ -0,0 +1,109 @@ +/// <reference path="../@types/network-information.d.ts" /> +import throttles from 'throttles'; +import requestIdleCallback from './requestIdleCallback.js'; + +const events = ['mouseenter', 'touchstart', 'focus']; + +const preloaded = new Set<string>(); + +function shouldPreload({ href }: { href: string }) { + try { + const url = new URL(href); + return ( + window.location.origin === url.origin && + window.location.pathname !== url.hash && + !preloaded.has(href) + ); + } catch {} + + return false; +} + +let parser: DOMParser; +let observer: IntersectionObserver; + +function observe(link: HTMLAnchorElement) { + preloaded.add(link.href); + observer.observe(link); + events.map((event) => link.addEventListener(event, onLinkEvent, { once: true })); +} + +function unobserve(link: HTMLAnchorElement) { + observer.unobserve(link); + events.map((event) => link.removeEventListener(event, onLinkEvent)); +} + +function onLinkEvent({ target }: Event) { + if (!(target instanceof HTMLAnchorElement)) { + return; + } + + preloadHref(target); +} + +async function preloadHref(link: HTMLAnchorElement) { + unobserve(link); + + const { href } = link; + + try { + const contents = await fetch(href).then((res) => res.text()); + parser = parser || new DOMParser(); + + const html = parser.parseFromString(contents, 'text/html'); + const styles = Array.from(html.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')); + + await Promise.all(styles.map(({ href }) => fetch(href))); + } catch {} +} + +export interface PrefetchOptions { + /** + * Element selector used to find all links on the page that should be prefetched. + * + * @default 'a[href][rel~="prefetch"]' + */ + selector?: string; + /** + * The number of pages that can be prefetched concurrently. + * + * @default 1 + */ + throttle?: number; +} + +export default function prefetch({ selector = 'a[href][rel~="prefetch"]', throttle = 1 }: PrefetchOptions) { + const conn = navigator.connection; + + if (typeof conn !== 'undefined') { + // Don't prefetch if using 2G or if Save-Data is enabled. + if (conn.saveData) { + return Promise.reject(new Error('Cannot prefetch, Save-Data is enabled')); + } + if (/2g/.test(conn.effectiveType)) { + return Promise.reject(new Error('Cannot prefetch, network conditions are poor')); + } + } + + const [toAdd, isDone] = throttles(throttle); + + observer = + observer || + new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting && entry.target instanceof HTMLAnchorElement) { + toAdd(() => preloadHref(entry.target as HTMLAnchorElement).finally(isDone)); + } + }); + }); + + requestIdleCallback(() => { + const links = Array.from(document.querySelectorAll<HTMLAnchorElement>(selector)).filter( + shouldPreload + ); + + for (const link of links) { + observe(link); + } + }); +} |