summaryrefslogtreecommitdiff
path: root/packages/integrations/prefetch/src
diff options
context:
space:
mode:
authorGravatar Tony Sullivan <tony.f.sullivan@outlook.com> 2022-06-27 18:26:21 +0000
committerGravatar GitHub <noreply@github.com> 2022-06-27 18:26:21 +0000
commit79fe09fa3093eb8ac2871c1894b9cddf557aecba (patch)
tree46a426e4adc3fc53bd8ba039cf277be5c8607f9e /packages/integrations/prefetch/src
parent9d78162fd9cab9eb2a64f58ca5703ba5d658828f (diff)
downloadastro-79fe09fa3093eb8ac2871c1894b9cddf557aecba.tar.gz
astro-79fe09fa3093eb8ac2871c1894b9cddf557aecba.tar.zst
astro-79fe09fa3093eb8ac2871c1894b9cddf557aecba.zip
Adds a prefetch integration for near-instant page navigations (#3725)
* Adds a basic @astrojs/prefetch integration * adding tests for custom selectors * missed in last commit * Adding a few docs, removing the option for `selectors` to be an element array * adding an option for the concurrency limit * fixing test for updated integration options * Update packages/labs/prefetch/src/client.ts Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com> * nit: removing the NodeJS.Timer type to allow typescript to infer the return * updating docs for default selector with ~= * Skip prefetching on 2G connections, or when data saver is enabled * refactor: moving to packages/integrations, Astro Labs TBD down the road * README typo fix Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
Diffstat (limited to 'packages/integrations/prefetch/src')
-rw-r--r--packages/integrations/prefetch/src/client.ts109
-rw-r--r--packages/integrations/prefetch/src/index.ts17
-rw-r--r--packages/integrations/prefetch/src/requestIdleCallback.ts16
3 files changed, 142 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);
+ }
+ });
+}
diff --git a/packages/integrations/prefetch/src/index.ts b/packages/integrations/prefetch/src/index.ts
new file mode 100644
index 000000000..15f52272c
--- /dev/null
+++ b/packages/integrations/prefetch/src/index.ts
@@ -0,0 +1,17 @@
+import type { AstroIntegration } from 'astro';
+import type { PrefetchOptions } from './client.js';
+
+export default function (options: PrefetchOptions = {}): AstroIntegration {
+ return {
+ name: '@astrojs/lit',
+ hooks: {
+ 'astro:config:setup': ({ updateConfig, addRenderer, injectScript }) => {
+ // Inject the necessary polyfills on every page (inlined for speed).
+ injectScript(
+ 'page',
+ `import prefetch from "@astrojs/prefetch/client.js"; prefetch(${JSON.stringify(options)});`
+ );
+ }
+ }
+ };
+}
diff --git a/packages/integrations/prefetch/src/requestIdleCallback.ts b/packages/integrations/prefetch/src/requestIdleCallback.ts
new file mode 100644
index 000000000..9435bd41d
--- /dev/null
+++ b/packages/integrations/prefetch/src/requestIdleCallback.ts
@@ -0,0 +1,16 @@
+function shim(callback: IdleRequestCallback, options?: IdleRequestOptions) {
+ const timeout = options?.timeout ?? 50;
+ const start = Date.now();
+
+ return setTimeout(function () {
+ callback({
+ didTimeout: false,
+ timeRemaining: function () {
+ return Math.max(0, timeout - (Date.now() - start));
+ },
+ });
+ }, 1);
+}
+
+const requestIdleCallback = window.requestIdleCallback || shim;
+export default requestIdleCallback;