feat: add prefetch utility
This commit is contained in:
parent
9cdada0bcc
commit
c610ff40e5
4 changed files with 198 additions and 3 deletions
|
@ -9,7 +9,7 @@
|
|||
".": "./astro.mjs",
|
||||
"./snowpack-plugin": "./snowpack-plugin.cjs",
|
||||
"./components/*": "./components/*",
|
||||
"./runtime/svelte": "./dist/frontend/runtime/svelte.js"
|
||||
"./runtime/*": "./dist/frontend/runtime/*.js"
|
||||
},
|
||||
"imports": {
|
||||
"#astro/compiler": "./dist/compiler/index.js",
|
||||
|
|
|
@ -6,9 +6,9 @@ import { walk } from 'estree-walker';
|
|||
// Transformers
|
||||
import transformStyles from './styles.js';
|
||||
import transformDoctype from './doctype.js';
|
||||
import optimizeLinks from './optimize-links.js';
|
||||
import transformModuleScripts from './module-scripts.js';
|
||||
import transformCodeBlocks from './prism.js';
|
||||
|
||||
interface VisitorCollection {
|
||||
enter: Map<string, VisitorFn[]>;
|
||||
leave: Map<string, VisitorFn[]>;
|
||||
|
@ -84,7 +84,7 @@ export async function transform(ast: Ast, opts: TransformOptions) {
|
|||
const cssVisitors = createVisitorCollection();
|
||||
const finalizers: Array<() => Promise<void>> = [];
|
||||
|
||||
const optimizers = [transformStyles(opts), transformDoctype(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
|
||||
const optimizers = [transformStyles(opts), transformDoctype(opts), optimizeLinks(opts), transformModuleScripts(opts), transformCodeBlocks(ast.module)];
|
||||
|
||||
for (const optimizer of optimizers) {
|
||||
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
||||
|
|
75
packages/astro/src/compiler/transform/optimize-links.ts
Normal file
75
packages/astro/src/compiler/transform/optimize-links.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import type { Transformer, TransformOptions } from '../../@types/transformer';
|
||||
import { getAttrValue } from '../../ast.js';
|
||||
|
||||
/** Transform <!doctype> tg */
|
||||
export default function (_opts: TransformOptions): Transformer {
|
||||
let hasAnyInternalLinks = false;
|
||||
|
||||
return {
|
||||
visitors: {
|
||||
html: {
|
||||
Element: {
|
||||
enter(node, parent, index) {
|
||||
if (hasAnyInternalLinks) return;
|
||||
let name = node.name;
|
||||
if (name !== 'a') {
|
||||
return;
|
||||
}
|
||||
|
||||
let href = getAttrValue(node.attributes, 'href');
|
||||
if (!href || href.startsWith('http')) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasAnyInternalLinks = true;
|
||||
},
|
||||
leave(node) {
|
||||
let name = node.name;
|
||||
if (name !== 'body') {
|
||||
return;
|
||||
}
|
||||
if (!hasAnyInternalLinks) {
|
||||
return;
|
||||
}
|
||||
|
||||
const prefetchScript = {
|
||||
start: 0,
|
||||
end: 0,
|
||||
type: 'Element',
|
||||
name: 'script',
|
||||
attributes: [
|
||||
{
|
||||
type: 'Attribute',
|
||||
name: 'type',
|
||||
value: [
|
||||
{
|
||||
type: 'Text',
|
||||
raw: 'module',
|
||||
data: 'module',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'Attribute',
|
||||
name: 'src',
|
||||
value: [
|
||||
{
|
||||
type: 'Text',
|
||||
raw: '/_astro_internal/runtime/prefetch.js',
|
||||
data: '/_astro_internal/runtime/prefetch.js',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
children: [],
|
||||
};
|
||||
|
||||
const newBody = { ...node, children: [...(node.children ?? []), prefetchScript] };
|
||||
this.replace(newBody);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
async finalize() {},
|
||||
};
|
||||
}
|
120
packages/astro/src/frontend/runtime/prefetch.ts
Normal file
120
packages/astro/src/frontend/runtime/prefetch.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
// Cache of URLs we've already prefetched
|
||||
const cache = new Set();
|
||||
const listeners = new Map<HTMLAnchorElement, (...args: any) => any>();
|
||||
const events = ["focus", "pointerenter"];
|
||||
|
||||
// RIC and shim for browsers setTimeout() without it
|
||||
const requestIdleCallback =
|
||||
(window as any).requestIdleCallback ||
|
||||
function (cb: (...args: any) => any) {
|
||||
const start = Date.now();
|
||||
return setTimeout(function () {
|
||||
cb({
|
||||
didTimeout: false,
|
||||
timeRemaining: function () {
|
||||
return Math.max(0, 50 - (Date.now() - start));
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches a given URL using `<link rel=prefetch>`
|
||||
* @param {string} url - the URL to fetch
|
||||
* @return {Promise<Event>} a Promise
|
||||
*/
|
||||
function addToHead(url: string): Promise<Event> {
|
||||
let link: HTMLLinkElement;
|
||||
return new Promise((res, rej) => {
|
||||
link = document.createElement(`link`);
|
||||
link.rel = `prefetch`;
|
||||
link.href = url;
|
||||
|
||||
link.onload = res;
|
||||
link.onerror = rej;
|
||||
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a Node is an HTMLAnchorElement
|
||||
* @param node DOM node to check
|
||||
*/
|
||||
function isAnchor(node: Node): node is HTMLAnchorElement {
|
||||
return (
|
||||
node.nodeType === node.ELEMENT_NODE && (node as Element).tagName === "A"
|
||||
);
|
||||
}
|
||||
|
||||
/** prefecth a given URL */
|
||||
function prefetch(url: string) {
|
||||
const conn = (window.navigator as any).connection;
|
||||
if (conn) {
|
||||
// Don't prefetch if using 2G or if Save-Data is enabled.
|
||||
if (conn.saveData || /2g/.test(conn.effectiveType)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.has(url)) {
|
||||
cache.add(url);
|
||||
return addToHead(new URL(url, window.location.href).toString());
|
||||
}
|
||||
}
|
||||
|
||||
/** Observe link visiblity and add listeners to prefetch the URL as needed */
|
||||
function listen() {
|
||||
if (!window.IntersectionObserver) return;
|
||||
|
||||
const io = new IntersectionObserver((entries) => {
|
||||
entries.forEach((_entry) => {
|
||||
if (_entry.isIntersecting) {
|
||||
const entry = _entry.target as HTMLAnchorElement;
|
||||
if (cache.has(entry.href)) return;
|
||||
|
||||
const cb = () => {
|
||||
const cleanup = () => {
|
||||
for (const event of events) {
|
||||
entry.removeEventListener(event, cb);
|
||||
}
|
||||
listeners.delete(entry);
|
||||
};
|
||||
prefetch(entry.href)
|
||||
?.then(() => cleanup())
|
||||
?.catch(() => cleanup());
|
||||
};
|
||||
listeners.set(entry, cb);
|
||||
for (const event of events) {
|
||||
entry.addEventListener(event, cb, { once: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const mo = new MutationObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!(entry.addedNodes.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const links = Array.from(entry.addedNodes).filter((node) =>
|
||||
isAnchor(node)
|
||||
) as HTMLAnchorElement[];
|
||||
if (!(links.length > 0)) return;
|
||||
links.forEach((link) => {
|
||||
io.observe(link);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
requestIdleCallback(() => {
|
||||
mo.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
document.querySelectorAll("a").forEach((link) => {
|
||||
io.observe(link);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
listen();
|
Loading…
Reference in a new issue