Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added CROSSORIGIN and eagerness PRERENDER #434

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .size-limit.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
[
{
"path": "dist/quicklink.js",
"limit": "2 kB",
"limit": "2.36 kB",
"gzip": true
},
{
"path": "dist/quicklink.mjs",
"limit": "2 kB",
"limit": "2.36 kB",
"gzip": true
},
{
"path": "dist/quicklink.modern.mjs",
"limit": "1.6 kB",
"limit": "1.91 kB",
"gzip": true
},
{
"path": "dist/quicklink.umd.js",
"limit": "2 kB",
"limit": "2.43 kB",
"gzip": true
}
]
]
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "quicklink",
"version": "2.3.0",
"version": "2.4.0",
"description": "Faster subsequent page-loads by prefetching in-viewport links during idle time",
"repository": {
"type": "git",
Expand Down Expand Up @@ -71,4 +71,4 @@
"size-limit": "^11.1.4",
"uvu": "^0.5.6"
}
}
}
4 changes: 2 additions & 2 deletions src/chunks.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
**/

import throttle from 'throttles';
import {priority, supported} from './prefetch.mjs';
import {viaFetch, supported} from './prefetch.mjs';
import requestIdleCallback from './request-idle-callback.mjs';

// Cache of URLs we've prefetched
Expand Down Expand Up @@ -145,7 +145,7 @@ export function prefetch(url, isPriority) {
// ~> so that we don't repeat broken links
toPrefetch.add(str);

return (isPriority ? priority : supported)(
return (isPriority ? viaFetch : supported)(
new URL(str, location.href).toString(),
);
}),
Expand Down
39 changes: 24 additions & 15 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
**/

import throttle from 'throttles';
import {priority, supported} from './prefetch.mjs';
import {prefetchOnHover, supported, viaFetch} from './prefetch.mjs';
import requestIdleCallback from './request-idle-callback.mjs';
import {addSpeculationRules, hasSpecRulesSupport} from './prerender.mjs';

Expand Down Expand Up @@ -72,6 +72,9 @@ function checkConnection(conn) {
* @param {Object} options - Configuration options for quicklink
* @param {Object|Array} [options.el] - DOM element(s) to prefetch in-viewport links of
* @param {Boolean} [options.priority] - Attempt higher priority fetch (low or high)
* @param {Boolean} [options.checkAccessControlAllowOrigin] - Check Access-Control-Allow-Origin response header
* @param {Boolean} [options.checkAccessControlAllowCredentials] - Check the Access-Control-Allow-Credentials response header
* @param {Boolean} [options.onlyOnMouseover] - Enable the prefetch only on mouseover event
* @param {Array} [options.origins] - Allowed origins to prefetch (empty allows all)
* @param {Array|RegExp|Function} [options.ignores] - Custom filter(s) that run after origin checks
* @param {Number} [options.timeout] - Timeout after which prefetching will occur
Expand All @@ -84,6 +87,7 @@ function checkConnection(conn) {
* @param {Function} [options.hrefFn] - Function to use to build the URL to prefetch.
* If it's not a valid function, then it will use the entry href.
* @param {Boolean} [options.prerender] - Option to switch from prefetching and use prerendering only
* @param {String} [options.eagerness] - Prerender eagerness mode - default immediate
* @param {Boolean} [options.prerenderAndPrefetch] - Option to use both prerendering and prefetching
* @return {Function}
*/
Expand Down Expand Up @@ -133,7 +137,7 @@ export function listen(options = {}) {
// either it's the prerender + prefetch mode or it's prerender *only* mode
// Prerendering limit is following options.limit. UA may impose arbitraty numeric limit
if ((shouldPrerenderAndPrefetch || shouldOnlyPrerender) && toPrerender.size < limit) {
prerender(hrefFn ? hrefFn(entry) : entry.href).catch(error => {
prerender(hrefFn ? hrefFn(entry) : entry.href, options.eagerness).catch(error => {
if (options.onError) {
options.onError(error);
} else {
Expand All @@ -147,7 +151,8 @@ export function listen(options = {}) {
// Do not prefetch if will match/exceed limit and user has not switched to shouldOnlyPrerender mode
if (toPrefetch.size < limit && !shouldOnlyPrerender) {
toAdd(() => {
prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority)
prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority,
options.checkAccessControlAllowOrigin, options.checkAccessControlAllowCredentials, options.onlyOnMouseover)
.then(isDone)
.catch(error => {
isDone();
Expand All @@ -156,7 +161,7 @@ export function listen(options = {}) {
});
}
}, delay);
// On exit
// On exit
} else {
entry = entry.target;
const index = hrefsInViewport.indexOf(entry.href);
Expand All @@ -172,9 +177,9 @@ export function listen(options = {}) {
timeoutFn(() => {
// Find all links & Connect them to IO if allowed
const elementsToListen = options.el &&
options.el.length &&
options.el.length > 0 &&
options.el[0].nodeName === 'A' ?
options.el.length &&
options.el.length > 0 &&
options.el[0].nodeName === 'A' ?
options.el :
(options.el || document).querySelectorAll('a');

Expand All @@ -201,10 +206,14 @@ export function listen(options = {}) {
/**
* Prefetch a given URL with an optional preferred fetch priority
* @param {String} url - the URL to fetch
* @param {Boolean} [isPriority] - if is "high" priority
* @param {Boolean} isPriority - if is "high" priority
* @param {Boolean} checkAccessControlAllowOrigin - true to set crossorigin="anonymous" for DOM prefetch
* and mode:'cors' for API fetch
* @param {Boolean} checkAccessControlAllowCredentials - true to set credentials:'include' for API fetch
* @param {Boolean} onlyOnMouseover - true to enable prefetch only on mouseover event
* @return {Object} a Promise
*/
export function prefetch(url, isPriority) {
export function prefetch(url, isPriority, checkAccessControlAllowOrigin, checkAccessControlAllowCredentials, onlyOnMouseover) {
const chkConn = checkConnection(navigator.connection);
if (chkConn instanceof Error) {
return Promise.reject(new Error(`Cannot prefetch, ${chkConn.message}`));
Expand All @@ -223,19 +232,19 @@ export function prefetch(url, isPriority) {
// ~> so that we don't repeat broken links
toPrefetch.add(str);

return (isPriority ? priority : supported)(
new URL(str, location.href).toString(),
);
return prefetchOnHover((isPriority ? viaFetch : supported), new URL(str, location.href).toString(), onlyOnMouseover,
checkAccessControlAllowOrigin, checkAccessControlAllowCredentials, isPriority);
}),
);
}

/**
* Prerender a given URL
* @param {String} urls - the URL to fetch
* @param {String} eagerness - prerender eagerness mode - default immediate
* @return {Object} a Promise
*/
export function prerender(urls) {
export function prerender(urls, eagerness = 'immediate') {
const chkConn = checkConnection(navigator.connection);
if (chkConn instanceof Error) {
return Promise.reject(new Error(`Cannot prerender, ${chkConn.message}`));
Expand All @@ -245,7 +254,7 @@ export function prerender(urls) {
// 1) whether UA supports spec rules.. If not, fallback to prefetch
// Note: Prerendering supports same-site cross origin with opt-in header
if (!hasSpecRulesSupport()) {
prefetch(urls);
prefetch(urls, true, false, false, eagerness === 'moderate' || eagerness === 'conservative');
return Promise.reject(new Error('This browser does not support the speculation rules API. Falling back to prefetch.'));
}

Expand All @@ -258,6 +267,6 @@ export function prerender(urls) {
console.warn('[Warning] You are using both prefetching and prerendering on the same document');
}

const addSpecRules = addSpeculationRules(toPrerender);
const addSpecRules = addSpeculationRules(toPrerender, eagerness);
return addSpecRules === true ? Promise.resolve() : Promise.reject(addSpecRules);
}
65 changes: 58 additions & 7 deletions src/prefetch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
/**
* Checks if a feature on `link` is natively supported.
* Examples of features include `prefetch` and `preload`.
* @param {Object} link Link object.
* @param {Object} link - Link object.
* @return {Boolean} whether the feature is supported
*/
function hasPrefetch(link) {
Expand All @@ -31,13 +31,17 @@ function hasPrefetch(link) {
/**
* Fetches a given URL using `<link rel=prefetch>`
* @param {string} url - the URL to fetch
* @param {Boolean} hasCrossorigin - true to set crossorigin="anonymous"
* @return {Object} a Promise
*/
function viaDOM(url) {
function viaDOM(url, hasCrossorigin) {
return new Promise((resolve, reject, link) => {
link = document.createElement('link');
link.rel = 'prefetch';
link.href = url;
if (hasCrossorigin) {
link.setAttribute('crossorigin', 'anonymous');
}

link.onload = resolve;
link.onerror = reject;
Expand All @@ -49,13 +53,16 @@ function viaDOM(url) {
/**
* Fetches a given URL using XMLHttpRequest
* @param {string} url - the URL to fetch
* @param {Boolean} hasCredentials - true to set withCredentials:true
* @return {Object} a Promise
*/
function viaXHR(url) {
function viaXHR(url, hasCredentials) {
return new Promise((resolve, reject, request) => {
request = new XMLHttpRequest();

request.open('GET', url, request.withCredentials = true);
request.open('GET', url, request.withCredentials = hasCredentials);

request.setRequestHeader('Accept', '*/*');

request.onload = () => {
if (request.status === 200) {
Expand All @@ -74,17 +81,61 @@ function viaXHR(url) {
* Fetches a given URL using the Fetch API. Falls back
* to XMLHttpRequest if the API is not supported.
* @param {string} url - the URL to fetch
* @param {Boolean} hasModeCors - true to set mode:'cors'
* @param {Boolean} hasCredentials - true to set credentials:'include'
* @param {Boolean} isPriority - true to set priority:'high'
* @return {Object} a Promise
*/
export function priority(url) {
export function viaFetch(url, hasModeCors, hasCredentials, isPriority) {
// TODO: Investigate using preload for high-priority
// fetches. May have to sniff file-extension to provide
// valid 'as' values. In the future, we may be able to
// use Priority Hints here.
//
// As of 2018, fetch() is high-priority in Chrome
// and medium-priority in Safari.
return window.fetch ? fetch(url, {credentials: 'include'}) : viaXHR(url);
const options = {headers: {accept: '*/*'}};
if (!hasModeCors) options.mode = 'no-cors';
if (hasCredentials) options.credentials = 'include';
isPriority ? options.priority = 'high' : options.priority = 'low';
return window.fetch ? fetch(url, options) : viaXHR(url, hasCredentials);
}

/**
* Calls the prefetch function immediately
* or only on the mouseover event.
* @param {Function} callback - original prefetch function
* @param {String} url - url to prefetch
* @param {Boolean} onlyOnMouseover - true to add the mouseover listener
* @return {Object} a Promise
*/
export function prefetchOnHover(callback, url, onlyOnMouseover, ...args) {
if (!onlyOnMouseover) return callback(url, ...args);

const elements = document.querySelectorAll(`a[href="${decodeURIComponent(url)}"]`);
const timerMap = new Map();

for (const el of elements) {
const mouseenterListener = e => {
const timer = setTimeout(() => {
el.removeEventListener('mouseenter', mouseenterListener);
el.removeEventListener('mouseleave', mouseleaveListener);
return callback(url, ...args);
}, 200);
timerMap.set(el, timer);
};

const mouseleaveListener = e => {
const timer = timerMap.get(el);
if (timer) {
clearTimeout(timer);
timerMap.delete(el);
}
};

el.addEventListener('mouseenter', mouseenterListener);
el.addEventListener('mouseleave', mouseleaveListener);
}
}

export const supported = hasPrefetch() ? viaDOM : viaXHR;
export const supported = hasPrefetch() ? viaDOM : viaFetch;
7 changes: 5 additions & 2 deletions src/prerender.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@
/**
* Add a given set of urls to the speculation rules
* @param {Set} urlsToPrerender - the URLs to add to speculation rules
* @param {String} eagerness - prerender eagerness mode
* @return {Boolean|Object} boolean or Error Object
*/
export function addSpeculationRules(urlsToPrerender) {
export function addSpeculationRules(urlsToPrerender, eagerness) {
const specScript = document.createElement('script');
specScript.type = 'speculationrules';
specScript.text = `{"prerender":[{"source": "list","urls": ["${Array.from(urlsToPrerender).join('","')}"]}]}`;
specScript.text = `{"prerender":[{"source": "list",
"urls": ["${Array.from(urlsToPrerender).join('","')}"],
"eagerness": "${eagerness}"}]}`;
try {
document.head.appendChild(specScript);
} catch (error) {
Expand Down