Compare commits

...

4 commits

Author SHA1 Message Date
Nate Moore
0393860169 feat: add linkPrefetch to config 2021-05-21 15:54:36 -05:00
Nate Moore
d107ae7e10 fix: use optimize-links on all pages 2021-05-21 15:54:36 -05:00
Nate Moore
c3b75b223e chore: fix portfolio build 2021-05-21 15:54:36 -05:00
Nate Moore
c610ff40e5 feat: add prefetch utility 2021-05-21 15:54:36 -05:00
7 changed files with 190 additions and 4 deletions

View file

@ -2,6 +2,7 @@
import MainHead from '../components/MainHead.astro';
import Footer from '../components/Footer/index.jsx';
import Nav from '../components/Nav/index.jsx';
import MainHead from '../components/MainHead.astro';
---
<html>

View file

@ -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",

View file

@ -28,6 +28,8 @@ export interface AstroConfig {
site?: string;
/** Generate sitemap (set to "false" to disable) */
sitemap: boolean;
/** Automatically prefetch internal <a> links (set to "false" to disable) */
linkPrefetch?: boolean;
};
/** Options for the development server run with `astro dev`. */
devOptions: {

View file

@ -13,7 +13,10 @@ export function getAttr(attributes: Attribute[], name: string): Attribute | unde
export function getAttrValue(attributes: Attribute[], name: string): string | undefined {
const attr = getAttr(attributes, name);
if (attr) {
return attr.value[0]?.data;
const child = attr.value[0];
if (!child) return;
if (child.type === 'Text') return child.data;
if (child.type === 'MustacheTag') return child.expression.codeChunks[0];
}
}

View file

@ -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);

View file

@ -0,0 +1,60 @@
import type { Transformer, TransformOptions } from '../../@types/transformer';
import { getAttrValue } from '../../ast.js';
/** Transform <!doctype> tg */
export default function (_opts: TransformOptions): Transformer {
return {
visitors: {
html: {
Element: {
leave(node) {
let name = node.name;
if (name !== 'body') {
return;
}
if (node.children?.find(el => el.name === 'script' && el.data.linkOptim)) return;
const prefetchScript = {
start: 0,
end: 0,
type: 'Element',
name: 'script',
data: {
linkOptim: true
},
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() {},
};
}

View 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();