1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
|
/// <reference types="../@types/network-information.d.ts" />
import throttles from 'throttles';
import requestIdleCallback from './requestIdleCallback.js';
const events = ['mouseenter', 'touchstart', 'focus'];
const preloaded = new Set<string>();
const loadedStyles = new Set<string>();
function shouldPreload({ href }: { href: string }) {
try {
const url = new URL(href);
return (
window.location.origin === url.origin &&
window.location.pathname !== url.pathname &&
!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, { passive: true, 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 ||= new DOMParser();
const html = parser.parseFromString(contents, 'text/html');
const styles = Array.from(html.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]'));
await Promise.all(
styles
.filter((el) => !loadedStyles.has(el.href))
.map((el) => {
loadedStyles.add(el.href);
return fetch(el.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) {
// If the navigator is offline, it is very unlikely that a request can be made successfully
if (!navigator.onLine) {
return Promise.reject(new Error('Cannot prefetch, no network connection'));
}
// `Navigator.connection` is an experimental API and is not supported in every browser.
if ('connection' in navigator) {
const connection = (navigator as any).connection;
// Don't prefetch if Save-Data is enabled.
if (connection.saveData) {
return Promise.reject(new Error('Cannot prefetch, Save-Data is enabled'));
}
// Do not prefetch if using 2G or 3G
if (/(2|3)g/.test(connection.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 = [...document.querySelectorAll<HTMLAnchorElement>(selector)].filter(shouldPreload);
links.forEach(observe);
});
}
|