/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
function isLikelyStaticResource(initiatorType: string) {
switch (initiatorType) {
case 'css':
case 'script':
case 'font':
case 'img':
case 'image':
case 'input':
case 'link':
return true;
default:
return false;
}
}
export default function estimateBandwidth(): number {
// Estimate the current bandwidth for downloading static resources given resources already
// loaded.
// $FlowFixMe[method-unbinding]
if (typeof performance.getEntriesByType === 'function') {
let count = 0;
let bits = 0;
const resourceEntries = performance.getEntriesByType('resource');
for (let i = 0; i < resourceEntries.length; i++) {
const entry = resourceEntries[i];
// $FlowFixMe[prop-missing]
const transferSize: number = entry.transferSize;
// $FlowFixMe[prop-missing]
const initiatorType: string = entry.initiatorType;
const duration = entry.duration;
if (
!transferSize ||
!duration ||
!isLikelyStaticResource(initiatorType)
) {
// Skip cached, cross-orgin entries and resources likely to be dynamically generated.
continue;
}
// Find any overlapping entries that were transferring at the same time since the total
// bps at the time will include those bytes.
let overlappingBytes = 0;
// $FlowFixMe[prop-missing]
const parentEndTime: number = entry.responseEnd;
let j;
for (j = i + 1; j < resourceEntries.length; j++) {
const overlapEntry = resourceEntries[j];
const overlapStartTime = overlapEntry.startTime;
if (overlapStartTime > parentEndTime) {
break;
}
// $FlowFixMe[prop-missing]
const overlapTransferSize: number = overlapEntry.transferSize;
// $FlowFixMe[prop-missing]
const overlapInitiatorType: string = overlapEntry.initiatorType;
if (
!overlapTransferSize ||
!isLikelyStaticResource(overlapInitiatorType)
) {
// Skip cached, cross-orgin entries and resources likely to be dynamically generated.
continue;
}
// $FlowFixMe[prop-missing]
const overlapEndTime: number = overlapEntry.responseEnd;
const overlapFactor =
overlapEndTime < parentEndTime
? 1
: (parentEndTime - overlapStartTime) /
(overlapEndTime - overlapStartTime);
overlappingBytes += overlapTransferSize * overlapFactor;
}
// Skip past any entries we already considered overlapping. Otherwise we'd have to go
// back to consider previous entries when we then handled them.
i = j - 1;
const bps =
((transferSize + overlappingBytes) * 8) / (entry.duration / 1000);
bits += bps;
count++;
if (count > 10) {
// We have enough to get an average.
break;
}
}
if (count > 0) {
return bits / count / 1e6;
}
}
// Fallback to the navigator.connection estimate if available
// $FlowFixMe[prop-missing]
if (navigator.connection) {
// $FlowFixMe
const downlink: ?number = navigator.connection.downlink;
if (typeof downlink === 'number') {
return downlink;
}
}
// Otherwise, use a default of 5mbps to compute heuristics.
// This can happen commonly in Safari if all static resources and images are loaded
// cross-orgin.
return 5;
}