Compare commits
4 commits
main
...
link-optim
Author | SHA1 | Date | |
---|---|---|---|
|
0393860169 | ||
|
d107ae7e10 | ||
|
c3b75b223e | ||
|
c610ff40e5 |
7 changed files with 190 additions and 4 deletions
|
@ -2,6 +2,7 @@
|
||||||
import MainHead from '../components/MainHead.astro';
|
import MainHead from '../components/MainHead.astro';
|
||||||
import Footer from '../components/Footer/index.jsx';
|
import Footer from '../components/Footer/index.jsx';
|
||||||
import Nav from '../components/Nav/index.jsx';
|
import Nav from '../components/Nav/index.jsx';
|
||||||
|
import MainHead from '../components/MainHead.astro';
|
||||||
---
|
---
|
||||||
|
|
||||||
<html>
|
<html>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
".": "./astro.mjs",
|
".": "./astro.mjs",
|
||||||
"./snowpack-plugin": "./snowpack-plugin.cjs",
|
"./snowpack-plugin": "./snowpack-plugin.cjs",
|
||||||
"./components/*": "./components/*",
|
"./components/*": "./components/*",
|
||||||
"./runtime/svelte": "./dist/frontend/runtime/svelte.js"
|
"./runtime/*": "./dist/frontend/runtime/*.js"
|
||||||
},
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"#astro/compiler": "./dist/compiler/index.js",
|
"#astro/compiler": "./dist/compiler/index.js",
|
||||||
|
|
|
@ -28,6 +28,8 @@ export interface AstroConfig {
|
||||||
site?: string;
|
site?: string;
|
||||||
/** Generate sitemap (set to "false" to disable) */
|
/** Generate sitemap (set to "false" to disable) */
|
||||||
sitemap: boolean;
|
sitemap: boolean;
|
||||||
|
/** Automatically prefetch internal <a> links (set to "false" to disable) */
|
||||||
|
linkPrefetch?: boolean;
|
||||||
};
|
};
|
||||||
/** Options for the development server run with `astro dev`. */
|
/** Options for the development server run with `astro dev`. */
|
||||||
devOptions: {
|
devOptions: {
|
||||||
|
|
|
@ -13,7 +13,10 @@ export function getAttr(attributes: Attribute[], name: string): Attribute | unde
|
||||||
export function getAttrValue(attributes: Attribute[], name: string): string | undefined {
|
export function getAttrValue(attributes: Attribute[], name: string): string | undefined {
|
||||||
const attr = getAttr(attributes, name);
|
const attr = getAttr(attributes, name);
|
||||||
if (attr) {
|
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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,9 @@ import { walk } from 'estree-walker';
|
||||||
// Transformers
|
// Transformers
|
||||||
import transformStyles from './styles.js';
|
import transformStyles from './styles.js';
|
||||||
import transformDoctype from './doctype.js';
|
import transformDoctype from './doctype.js';
|
||||||
|
import optimizeLinks from './optimize-links.js';
|
||||||
import transformModuleScripts from './module-scripts.js';
|
import transformModuleScripts from './module-scripts.js';
|
||||||
import transformCodeBlocks from './prism.js';
|
import transformCodeBlocks from './prism.js';
|
||||||
|
|
||||||
interface VisitorCollection {
|
interface VisitorCollection {
|
||||||
enter: Map<string, VisitorFn[]>;
|
enter: Map<string, VisitorFn[]>;
|
||||||
leave: Map<string, VisitorFn[]>;
|
leave: Map<string, VisitorFn[]>;
|
||||||
|
@ -84,7 +84,7 @@ export async function transform(ast: Ast, opts: TransformOptions) {
|
||||||
const cssVisitors = createVisitorCollection();
|
const cssVisitors = createVisitorCollection();
|
||||||
const finalizers: Array<() => Promise<void>> = [];
|
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) {
|
for (const optimizer of optimizers) {
|
||||||
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
collectVisitors(optimizer, htmlVisitors, cssVisitors, finalizers);
|
||||||
|
|
60
packages/astro/src/compiler/transform/optimize-links.ts
Normal file
60
packages/astro/src/compiler/transform/optimize-links.ts
Normal 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() {},
|
||||||
|
};
|
||||||
|
}
|
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…
Add table
Reference in a new issue