Skip to content

Commit

Permalink
[feat] delay option to reduce impact on CDNs and servers [alternati…
Browse files Browse the repository at this point in the history
…ve without data-attributes] (#217)

* Added `delay` option without data attributes

* Bugfix: landing links were not prefetched

* Fix exit condition removing unused `const`

* Added `options.delay` to api.njk
  • Loading branch information
verlok authored Jan 21, 2021
1 parent 5205d62 commit 5cdf569
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 7 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ Returns: `Function`
A "reset" function is returned, which will empty the active `IntersectionObserver` and the cache of URLs that have already been prefetched. This can be used between page navigations and/or when significant DOM changes have occurred.
#### options.delay
Type: `Number`<br>
Default: `0`
The _amount of time_ each link needs to stay inside the viewport before being prefetched, in milliseconds.
#### options.el
Type: `HTMLElement`<br>
Default: `document.body`
Expand Down
6 changes: 6 additions & 0 deletions site/src/api.njk
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ Default: `document.body`

The DOM element to observe for in-viewport links to prefetch.

#### options.delay
Type: `Number`<br>
Default: `0`

The _amount of time_ each link needs to stay inside the viewport before being prefetched, in milliseconds.

#### options.limit
Type: `Number`<br>
Default: `Infinity`
Expand Down
44 changes: 37 additions & 7 deletions src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function isIgnored(node, filter) {
* @param {Number} [options.timeout] - Timeout after which prefetching will occur
* @param {Number} [options.throttle] - The concurrency limit for prefetching
* @param {Number} [options.limit] - The total number of prefetches to allow
* @param {Number} [options.delay] - Time each link needs to stay inside viewport before prefetching (milliseconds)
* @param {Function} [options.timeoutFn] - Custom timeout function
* @param {Function} [options.onError] - Error handler for failed `prefetch` requests
* @param {Function} [options.hrefFn] - Function to use to build the URL to prefetch.
Expand All @@ -65,21 +66,50 @@ export function listen(options) {

const allowed = options.origins || [location.hostname];
const ignores = options.ignores || [];
const delay = options.delay || 0;
const hrefsInViewport = [];

const timeoutFn = options.timeoutFn || requestIdleCallback;
const hrefFn = typeof options.hrefFn === 'function' && options.hrefFn;

const setTimeoutIfDelay = (callback, delay) => {
if (!delay) {
callback();
return;
}
setTimeout(callback, delay);
}

const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
// On enter
if (entry.isIntersecting) {
observer.unobserve(entry = entry.target);
// Do not prefetch if will match/exceed limit
if (toPrefetch.size < limit) {
toAdd(() => {
prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority).then(isDone).catch(err => {
isDone(); if (options.onError) options.onError(err);
entry = entry.target;
// Adding href to array of hrefsInViewport
hrefsInViewport.push(entry.href);

// Setting timeout
setTimeoutIfDelay(() => {
// Do not prefetch if not found in viewport
if (hrefsInViewport.indexOf(entry.href) === -1) return;

observer.unobserve(entry);
// Do not prefetch if will match/exceed limit
if (toPrefetch.size < limit) {
toAdd(() => {
prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority).then(isDone).catch(err => {
isDone(); if (options.onError) options.onError(err);
});
});
});
}
}, delay);
}
// On exit
else {
entry = entry.target;
const index = hrefsInViewport.indexOf(entry.href);
if (index > -1) {
hrefsInViewport.splice(index);
}
}
});
Expand Down
29 changes: 29 additions & 0 deletions test/quicklink.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,4 +274,33 @@ describe('quicklink tests', function () {
expect(ours).to.include(`https://example.com/?url=${server}/3.html`);
expect(ours).to.include(`https://example.com/?url=${server}/4.html`);
});

it('should delay prefetch for in-viewport links correctly (UMD)', async function () {
const responseURLs = [];
page.on('response', resp => {
responseURLs.push(resp.url());
});
await page.goto(`${server}/test-delay.html`);
await page.waitFor(1000);
expect(responseURLs).to.be.an('array');
expect(responseURLs).to.include(`${server}/1.html`);
expect(responseURLs).to.include(`${server}/2.html`);
expect(responseURLs).to.include(`${server}/3.html`);
// Scroll down and up
await page.evaluate(_ => {
window.scrollBy(0, window.innerHeight);
});
await page.waitFor(100);
await page.evaluate(_ => {
window.scrollBy(0, -window.innerHeight);
});
expect(responseURLs).not.to.include(`${server}/4.html`);
// Scroll down and test
await page.evaluate(_ => {
window.scrollBy(0, window.innerHeight);
});
await page.waitFor(200);
expect(responseURLs).to.include(`${server}/4.html`);
});

});
25 changes: 25 additions & 0 deletions test/test-delay.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Prefetch: Basic Usage</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" media="screen" href="main.css" />
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
</head>

<body>
<a href="1.html">Link 1</a>
<a href="2.html">Link 2</a>
<a href="3.html">Link 3</a>
<section id="stuff">
<a href="main.css">CSS</a>
</section>
<a href="4.html" style="position: absolute; margin-top: 900px">Link 4</a>
<script src="../dist/quicklink.umd.js"></script>
<script>
quicklink.listen({ delay: 200 });
</script>
</body>
</html>

0 comments on commit 5cdf569

Please sign in to comment.