[chrome 120+?][validation] Prerendering can suspend the loading of the entry page for 30 seconds
Status - Fix revision 4
suspend prerendering just before Connect request to Integrity Service
resume on short suspension of prerendering
reload on long suspension of prerendering
threshold is 2000ms
Root Cause
In prerendering, which is triggered by hovering the mouse cursor over the bookmark to the entry page, the page starts validation but is not ready for further processing like service worker registration, etc.
Workarounds
Avoid hovering of the mouse cursor over the entry page bookmark and enter the URL in the address bar instead
or
Disable page preloading in chrome://settings/performance
If the suspension of prerendering lasts longer than the threshold, the entry page is reloaded
The hovering period of the mouse cursor during prerendering should be short
Special values
-1 - negative value to always resume rendering after the suspension. Infinity will do as well.
1e-10 - small fraction of positive milliseconds to always reload after suspension of prerendering
Note: If ?? (nullish coalescing operator) were used instead of ||, 0 could be used for this purpose but not supported with this fix.
Handle race conditions
The values of document.prerendering and the order of evalutating document.prerendering can vary
The decided action on prerenderingchange has to be propagated reliably from integrity.js to cache-bundle.js
If the cache is empty at start, it has to be empty when the page is reloaded
If the cache has been loaded at start, no handling of prerendering is required on cache-bundle.js side
action-on-prerenderingchange event is always fired regardless of prerendering
action is stored at hook.parameters.actionOnPrerenderingChange so that cache-bundle.js can get the action even if the script has missed the action-on-prerenderingchange event
hook.parameters.actionOnPrerenderingChange does NOT affect the browserHash value
diff --git a/plugins/cache-bundle-js/cache-bundle.js b/plugins/cache-bundle-js/cache-bundle.js
index d74d28ae..4b7ee3d6 100644
--- a/plugins/cache-bundle-js/cache-bundle.js
+++ b/plugins/cache-bundle-js/cache-bundle.js
@@ -401,6 +401,41 @@ else if (enableCacheBundle) {
switch (cacheStatus.status) {
default:
case 'load': // transition to loading state
await caches.delete(version); // delete the empty cache temporarily
cache = null;
if (document.prerendering) {
console.log(cache-bundle.js: awaiting while document is in prerendering);
[chrome 120+?][validation] Prerendering can suspend the loading of the entry page for 30 seconds
Status - Fix revision 4
Root Cause
Workarounds
Alternative Fix - Not working as expected
Fix revision 4 - Resume on short suspension (< 2000ms ~3000ms~) of prerendering. Reload on long suspension. Prerendering is too hasty anyway.
Suspend prerendering
hook.parameters.agedPrerenderingThreshold
- default value:2000
(ms)-1
- negative value to always resume rendering after the suspension.Infinity
will do as well.1e-10
- small fraction of positive milliseconds to always reload after suspension of prerendering??
(nullish coalescing operator) were used instead of||
,0
could be used for this purpose but not supported with this fix.Handle race conditions
document.prerendering
and the order of evalutatingdocument.prerendering
can varyintegrity.js
tocache-bundle.js
cache-bundle.js
sideaction-on-prerenderingchange
event is always fired regardless of prerenderinghook.parameters.actionOnPrerenderingChange
so thatcache-bundle.js
can get the action even if the script has missed theaction-on-prerenderingchange
eventhook.parameters.actionOnPrerenderingChange
does NOT affect thebrowserHash
valueawait caches.delete(version); // delete the empty cache temporarily
cache = null;
if (document.prerendering) {
console.log(
cache-bundle.js: awaiting while document is in prerendering
);await new Promise((resolve, reject) => {
let listener;
document.addEventListener('prerenderingchange', listener = () => {
if (!document.prerendering) {
document.removeEventListener('prerenderingchange', listener);
resolve();
}
});
});
}
const action = await new Promise((resolve, reject) => {
if (hook.parameters.actionOnPrerenderingChange) {
resolve(hook.parameters.actionOnPrerenderingChange);
}
else {
let listener;
document.addEventListener('action-on-prerenderingchange', listener = (event) => {
document.removeEventListener('action-on-prerenderingchange', listener);
resolve(event.detail.name);
});
}
});
switch (action) {
case 'resume':
default:
cache = await caches.open(version);
break;
case 'reload':
await new Promise((resolve, reject) => { / never settles / });
break;
} cacheStatus.status = 'loading'; await cache.put(new Request(CACHE_STATUS_PSEUDO_URL), new Response(JSON.stringify(cacheStatus), { headers: { 'Content-Type': 'application/json', 'User-Agent': navigator.userAgent }})); break; diff --git a/plugins/integrity-js/integrity.js b/plugins/integrity-js/integrity.js index e10799cb..8dc7557b 100644 --- a/plugins/integrity-js/integrity.js +++ b/plugins/integrity-js/integrity.js @@ -2257,6 +2257,45 @@ crypto.getRandomValues(new Uint8Array(CurrentSession.connect_salt)); delete CurrentSession.connect_salt;
// suspend prerendering just before Connect request
let action = 'resume'; // default action
if (document.prerendering) {
const beforePrerenderingSuspension = performance.now();
console.log(
integrity.js: awaiting while document is in prerendering
);await new Promise((resolve, reject) => {
let listener;
document.addEventListener('prerenderingchange', listener = () => {
if (!document.prerendering) {
document.removeEventListener('prerenderingchange', listener);
resolve();
}
});
});
const afterPrerenderingSuspension = performance.now();
const DEFAULT_AGED_PRERENDERING_THRESHOLD = 2000; // ms
const agedPrerenderingThreshold = hook.parameters.agedPrerenderingThreshold || DEFAULT_AGED_PRERENDERING_THRESHOLD;
const suspensionPeriod = afterPrerenderingSuspension - beforePrerenderingSuspension;
if (agedPrerenderingThreshold > 0) {
if (suspensionPeriod > agedPrerenderingThreshold) {
action = 'reload';
}
}
console.log(
integrity.js: prerenderingchange action = ${action}
);}
// for scripts that will miss the following event notification
hook.parameters.actionOnPrerenderingChange = action; // Note: this assignment does not affect browserHash
// notify other scripts of the action whether prerendering is triggered or not
document.dispatchEvent(new CustomEvent('action-on-prerenderingchange', { detail: { name: action } }));
switch (action) {
case 'reload':
location.reload();
await new Promise((resolve, reject) => { / never settles / });
break;
case 'resume':
default:
break;
}