Skip to content

Commit 805477a

Browse files
giorgiopellegrinoXhmikosRwestonruter
authored
Improve speculation rules handling based on element visibility (#446)
* added logic to add and remove speculation in viewport * Removal of unnecessary checks to save space * added condition for unobserver if it is not prerender immediate * fix immediate condition on unobserver call * Update src/prerender.mjs Changed "urlsToPrerender" property type on JSDoc Co-authored-by: Weston Ruter <[email protected]> * Update src/prerender.mjs Error return type object Co-authored-by: Weston Ruter <[email protected]> * Update src/index.mjs const variable specRulesInViewport Co-authored-by: Weston Ruter <[email protected]> * Update README.md Added functionality to the listen({ prerender: true }) method to allow prerendering only of content within the viewport, and introduced the eagerness property to the prerender method. * Update README.md Co-authored-by: Weston Ruter <[email protected]> * Update src/prerender.mjs Co-authored-by: Weston Ruter <[email protected]> * Update src/prerender.mjs Co-authored-by: Weston Ruter <[email protected]> * Update src/index.mjs Co-authored-by: Weston Ruter <[email protected]> * Update README.md Co-authored-by: Weston Ruter <[email protected]> * Update README.md Co-authored-by: Weston Ruter <[email protected]> --------- Co-authored-by: XhmikosR <[email protected]> Co-authored-by: Weston Ruter <[email protected]>
1 parent 5f23ff2 commit 805477a

File tree

3 files changed

+82
-23
lines changed

3 files changed

+82
-23
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,14 @@ A "reset" function is returned, which will empty the active `IntersectionObserve
126126
Whether to switch from the default prefetching mode to the prerendering mode for the links inside the viewport.
127127

128128
> **Note:** The prerendering mode (when this option is set to true) will fallback to the prefetching mode if the browser does not support prerender.
129+
> Once the element exits the viewport, the `speculationrules` script is removed from the DOM. This approach makes it possible to exceed the limit of 10 prerenders imposed for the 'immediate' and 'eager' settings for eagerness.
130+
131+
#### options.eagerness
132+
133+
- Type: `String`
134+
- Default: `immediate`
135+
136+
Determines the mode to be used for prerendering specified within the speculation rules.
129137

130138
#### options.prerenderAndPrefetch
131139

@@ -266,7 +274,7 @@ By default, calls to `prefetch()` are low priority.
266274

267275
> **Note:** This behaves identically to `listen()`'s `priority` option.
268276
269-
### quicklink.prerender(urls)
277+
### quicklink.prerender(urls, eagerness)
270278

271279
Returns: `Promise`
272280

@@ -281,6 +289,13 @@ One or many URLs to be prerendered.
281289

282290
> **Note:** Speculative Rules API supports same-site cross origin Prerendering with [opt-in header](https://bit.ly/ss-cross-origin-pre).
283291
292+
#### eagerness
293+
294+
- Type: `String`
295+
- Default: `immediate`
296+
297+
Determines the mode to be used for prerendering specified within the speculation rules.
298+
284299
## Polyfills
285300

286301
`quicklink`:

src/index.mjs

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import throttle from 'throttles';
1818
import {prefetchOnHover, supported, viaFetch} from './prefetch.mjs';
1919
import requestIdleCallback from './request-idle-callback.mjs';
20-
import {addSpeculationRules, hasSpecRulesSupport} from './prerender.mjs';
20+
import {addSpeculationRules, removeSpeculationRule, hasSpecRulesSupport} from './prerender.mjs';
2121

2222
// Cache of URLs we've prefetched
2323
// Its `size` is compared against `opts.limit` value.
@@ -102,6 +102,7 @@ export function listen(options = {}) {
102102
const ignores = options.ignores || [];
103103
const delay = options.delay || 0;
104104
const hrefsInViewport = [];
105+
const specRulesInViewport = new Map();
105106

106107
const timeoutFn = options.timeoutFn || requestIdleCallback;
107108
const hrefFn = typeof options.hrefFn === 'function' && options.hrefFn;
@@ -131,19 +132,28 @@ export function listen(options = {}) {
131132
// Do not prefetch if not found in viewport
132133
if (!hrefsInViewport.includes(entry.href)) return;
133134

134-
observer.unobserve(entry);
135+
if (!shouldOnlyPrerender && !shouldPrerenderAndPrefetch) {
136+
observer.unobserve(entry);
137+
}
135138

136139
// prerender, if..
137140
// either it's the prerender + prefetch mode or it's prerender *only* mode
138141
// Prerendering limit is following options.limit. UA may impose arbitraty numeric limit
139-
if ((shouldPrerenderAndPrefetch || shouldOnlyPrerender) && toPrerender.size < limit) {
140-
prerender(hrefFn ? hrefFn(entry) : entry.href, options.eagerness).catch(error => {
141-
if (options.onError) {
142-
options.onError(error);
143-
} else {
144-
throw error;
145-
}
146-
});
142+
// The same URL is not already present as a speculation rule
143+
if ((shouldPrerenderAndPrefetch || shouldOnlyPrerender) && toPrerender.size < limit && !specRulesInViewport.has(entry.href)) {
144+
prerender(hrefFn ? hrefFn(entry) : entry.href, options.eagerness)
145+
.then(specMap => {
146+
for (const [key, value] of specMap) {
147+
specRulesInViewport.set(key, value);
148+
}
149+
})
150+
.catch(error => {
151+
if (options.onError) {
152+
options.onError(error);
153+
} else {
154+
throw error;
155+
}
156+
});
147157

148158
return;
149159
}
@@ -168,6 +178,9 @@ export function listen(options = {}) {
168178
if (index > -1) {
169179
hrefsInViewport.splice(index);
170180
}
181+
if (specRulesInViewport.has(entry.href)) {
182+
specRulesInViewport = removeSpeculationRule(specRulesInViewport, entry.href);
183+
}
171184
}
172185
});
173186
}, {
@@ -245,6 +258,8 @@ export function prefetch(urls, isPriority, checkAccessControlAllowOrigin, checkA
245258
* @return {Object} a Promise
246259
*/
247260
export function prerender(urls, eagerness = 'immediate') {
261+
urls = [].concat(urls);
262+
248263
const chkConn = checkConnection(navigator.connection);
249264
if (chkConn instanceof Error) {
250265
return Promise.reject(new Error(`Cannot prerender, ${chkConn.message}`));
@@ -258,7 +273,7 @@ export function prerender(urls, eagerness = 'immediate') {
258273
return Promise.reject(new Error('This browser does not support the speculation rules API. Falling back to prefetch.'));
259274
}
260275

261-
for (const url of [].concat(urls)) {
276+
for (const url of urls) {
262277
toPrerender.add(url);
263278
}
264279

@@ -267,6 +282,6 @@ export function prerender(urls, eagerness = 'immediate') {
267282
console.warn('[Warning] You are using both prefetching and prerendering on the same document');
268283
}
269284

270-
const addSpecRules = addSpeculationRules(toPrerender, eagerness);
271-
return addSpecRules === true ? Promise.resolve() : Promise.reject(addSpecRules);
285+
const specMap = addSpeculationRules(urls, eagerness);
286+
return specMap.size > 0 ? Promise.resolve(specMap) : Promise.reject(specMap);
272287
}

src/prerender.mjs

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,52 @@
1919

2020
/**
2121
* Add a given set of urls to the speculation rules
22-
* @param {Set} urlsToPrerender - the URLs to add to speculation rules
22+
* @param {String[]} urlsToPrerender - the URLs to add to speculation rules
2323
* @param {String} eagerness - prerender eagerness mode
24-
* @return {Boolean|Object} boolean or Error Object
24+
* @return {Map<HTMLScriptElement, string>|Error} Map of script elements to their URLs or Error Object
2525
*/
2626
export function addSpeculationRules(urlsToPrerender, eagerness) {
27-
const specScript = document.createElement('script');
28-
specScript.type = 'speculationrules';
29-
specScript.text = `{"prerender":[{"source": "list",
30-
"urls": ["${Array.from(urlsToPrerender).join('","')}"],
31-
"eagerness": "${eagerness}"}]}`;
27+
const specMap = new Map();
28+
29+
try {
30+
for (const url of urlsToPrerender) {
31+
const specScript = document.createElement('script');
32+
specScript.type = 'speculationrules';
33+
specScript.text = JSON.stringify({
34+
prerender: [{
35+
source: 'list',
36+
urls: [url],
37+
eagerness,
38+
}],
39+
});
40+
41+
document.head.appendChild(specScript);
42+
specMap.set(url, specScript);
43+
}
44+
} catch (error) {
45+
return error;
46+
}
47+
48+
return specMap;
49+
}
50+
51+
/**
52+
* Removes a speculation rule script associated with a given URL
53+
* @param {Map<string, HTMLScriptElement>} specMap - Map of URLs to their script elements
54+
* @param {string} url - The URL whose speculation rule should be removed
55+
* @return {Map<string, HTMLScriptElement>|Error} The updated map after removal or Error Object
56+
*/
57+
export function removeSpeculationRule(specMap, url) {
58+
const specScript = specMap.get(url);
59+
3260
try {
33-
document.head.appendChild(specScript);
61+
specScript.remove();
62+
specMap.delete(url);
3463
} catch (error) {
3564
return error;
3665
}
3766

38-
return true;
67+
return specMap;
3968
}
4069

4170
/**

0 commit comments

Comments
 (0)