log("loading chrome.load.times.js");
if (!window.chrome) {
// Use the exact property descriptor found in headful Chrome
// fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
Object.defineProperty(window, "chrome", {
writable: true,
enumerable: true,
configurable: false, // note!
value: {} // We'll extend that later
});
}
// That means we're running headful and don't need to mock anything
if ("loadTimes" in window.chrome) {
warn("skipping chrome loadtimes update, running in headful mode");
}
// Check that the Navigation Timing API v1 + v2 is available, we need that
if (window.performance?.timing || window.PerformancePaintTiming) {
const { performance } = window;
// Some stuff is not available on about:blank as it requires a navigation to occur,
// let's harden the code to not fail then:
const ntEntryFallback = {
nextHopProtocol: "h2",
type: "other"
};
// The API exposes some funky info regarding the connection
const protocolInfo = {
get connectionInfo() {
const ntEntry = performance.getEntriesByType("navigation")[0] ||
ntEntryFallback;
return ntEntry.nextHopProtocol;
},
get npnNegotiatedProtocol() {
// NPN is deprecated in favor of ALPN, but this implementation returns the
// HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
const ntEntry = performance.getEntriesByType("navigation")[0] ||
ntEntryFallback;
return ["h2", "hq"].includes(ntEntry.nextHopProtocol) ?
ntEntry.nextHopProtocol :
"unknown";
},
get navigationType() {
const ntEntry = performance.getEntriesByType("navigation")[0] ||
ntEntryFallback;
return ntEntry.type;
},
get wasAlternateProtocolAvailable() {
// The Alternate-Protocol header is deprecated in favor of Alt-Svc
// (https://www.mnot.net/blog/2016/03/09/alt-svc), so technically this
// should always return false.
return false;
},
get wasFetchedViaSpdy() {
// SPDY is deprecated in favor of HTTP/2, but this implementation returns
// true for HTTP/2 or HTTP2+QUIC/39 as well.
const ntEntry = performance.getEntriesByType("navigation")[0] ||
ntEntryFallback;
return ["h2", "hq"].includes(ntEntry.nextHopProtocol);
},
get wasNpnNegotiated() {
// NPN is deprecated in favor of ALPN, but this implementation returns true
// for HTTP/2 or HTTP2+QUIC/39 requests negotiated via ALPN.
const ntEntry = performance.getEntriesByType("navigation")[0] ||
ntEntryFallback;
return ["h2", "hq"].includes(ntEntry.nextHopProtocol);
}
};
const { timing } = window.performance;
// Truncate number to specific number of decimals, most of the `loadTimes` stuff has 3
function toFixed(num, fixed) {
const re = new RegExp("^-?\\d+(?:.\\d{0," + (fixed || -1) + "})?");
return num.toString().match(re)[0];
}
const timingInfo = {
get firstPaintAfterLoadTime() {
// This was never actually implemented and always returns 0.
return 0;
},
get requestTime() {
return timing.navigationStart / 1000;
},
get startLoadTime() {
return timing.navigationStart / 1000;
},
get commitLoadTime() {
return timing.responseStart / 1000;
},
get finishDocumentLoadTime() {
return timing.domContentLoadedEventEnd / 1000;
},
get finishLoadTime() {
return timing.loadEventEnd / 1000;
},
get firstPaintTime() {
const fpEntry = performance.getEntriesByType("paint")[0] || {
startTime: timing.loadEventEnd / 1000 // Fallback if no navigation occured (`about:blank`)
};
return toFixed((fpEntry.startTime + performance.timeOrigin) / 1000, 3);
}
};
window.chrome.loadTimes = function() {
return {
...protocolInfo,
...timingInfo
};
};
utils.patchToString(window.chrome.loadTimes);
}