@hint/hint-performance-budget
Version:
hint that that checks if a page passes a set performance budget
160 lines (159 loc) • 6.97 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const url_1 = require("url");
const utils_network_1 = require("@hint/utils-network");
const utils_debug_1 = require("@hint/utils-debug");
const utils_types_1 = require("@hint/utils-types");
const Connections = require("./connections");
const meta_1 = require("./meta");
const i18n_import_1 = require("./i18n.import");
const debug = (0, utils_debug_1.debug)(__filename);
const defaultConfig = {
connectionType: '3GFast',
loadTime: 5
};
class PerformanceBudgetHint {
constructor(context) {
const responses = [];
const uniqueDomains = new Set();
const secureDomains = new Set();
let performedRedirects = 0;
let performedRequests = 0;
const updateDomainsInfo = (resource) => {
const resourceUrl = new url_1.URL(resource);
uniqueDomains.add(resourceUrl.hostname);
if ((0, utils_network_1.isHTTPS)(resource)) {
secureDomains.add(resourceUrl.hostname);
}
};
const updateCounters = (response) => {
performedRedirects += response.hops.length;
performedRequests++;
};
const updateSizes = async (resource, response) => {
const uncompressedSize = response.body.rawContent ?
response.body.rawContent.byteLength :
response.body.content.length;
let sentSize;
const contentEncoding = (0, utils_network_1.normalizeHeaderValue)(response.headers, 'content-encoding', '');
if (contentEncoding) {
try {
sentSize = (await response.body.rawResponse()).byteLength;
}
catch (e) {
debug(`Error trying to get the \`rawResponse\` for ${resource}. Using uncompressedSize instead`);
debug(e);
sentSize = uncompressedSize;
}
}
else {
sentSize = uncompressedSize;
}
responses.push({
resource,
sentSize,
uncompressedSize
});
};
const onFetchEnd = async (fetchEnd) => {
debug(`Validating hint Performance budget`);
const { resource, response } = fetchEnd;
updateDomainsInfo(resource);
updateCounters(response);
await updateSizes(resource, response);
};
const getConfiguration = () => {
const userConfig = Object.assign({}, defaultConfig, context.hintOptions);
const config = Connections.getById(userConfig.connectionType || '');
if (config) {
config.load = userConfig.loadTime;
}
return config;
};
const calculateTotalDNSLookUp = (domains, config) => {
const dnsLookUpTime = config.latency;
const total = domains.size * dnsLookUpTime / 1000;
debug(`Total DNS lookup time: ${total}`);
return total;
};
const calculateTotalTCPHandshake = (connections, config) => {
const time = connections * config.latency / 1000;
return time;
};
const calculateTotalTLSHandshaking = (domains, config) => {
const tlsHandshakingTime = config.latency;
const total = domains.size * tlsHandshakingTime / 1000;
debug(`Total TLS handshake time: ${total}`);
return total;
};
const calculateTotalRedirectTime = (redirects, config) => {
const total = redirects * config.latency / 1000;
debug(`Total redirect time: ${total}`);
return total;
};
const calculateTransferTimeForResource = (response, config) => {
const networkSegmentSize = 1460;
const rwnd = 65535;
const cwnd = 10;
const dataInFlight = Math.min(response.sentSize, rwnd);
const segments = Math.ceil(dataInFlight / networkSegmentSize);
const time = config.latency * (Math.log2(1 + segments / cwnd)) / 1000;
if (response.sentSize < rwnd) {
return time;
}
return (response.sentSize - rwnd) * 8 / config.bwIn + time;
};
const calculateTransferTimeWithSlowStart = (allResponses, config) => {
const totalTime = allResponses.reduce((time, resource) => {
const transfertTime = calculateTransferTimeForResource(resource, config);
return time + transfertTime;
}, 0);
return totalTime;
};
const getBestCaseScenario = (config) => {
const dnsLookUpTime = calculateTotalDNSLookUp(uniqueDomains, config);
const tcpHandshakeTime = calculateTotalTCPHandshake(performedRequests, config);
const tlsHandshakeTime = calculateTotalTLSHandshaking(secureDomains, config);
const redirectTime = calculateTotalRedirectTime(performedRedirects, config);
const transferTimeSlowStart = calculateTransferTimeWithSlowStart(responses, config);
const total = dnsLookUpTime +
tcpHandshakeTime +
tlsHandshakeTime +
redirectTime +
transferTimeSlowStart;
return total;
};
const calculateSeverity = (loadTime, configurationLoadTime) => {
const percentage = (loadTime * 100) / configurationLoadTime;
if (percentage < 90) {
return utils_types_1.Severity.off;
}
else if (percentage < 100) {
return utils_types_1.Severity.hint;
}
else if (percentage < 150) {
return utils_types_1.Severity.warning;
}
return utils_types_1.Severity.error;
};
const onScanEnd = (scanEnd) => {
const { resource } = scanEnd;
const config = getConfiguration();
if (!config) {
return;
}
const loadTime = getBestCaseScenario(config);
debug(`Ideal load time: ${loadTime}s`);
if (typeof config.load === 'number') {
const severity = calculateSeverity(loadTime, config.load);
if (severity !== utils_types_1.Severity.off) {
context.report(resource, (0, i18n_import_1.getMessage)(severity === utils_types_1.Severity.hint ? 'toLoadAllResourcesLess' : 'toLoadAllResourcesMore', context.language, [config.id, loadTime.toFixed(1), (loadTime - config.load).toFixed(1), config.load.toString()]), { severity });
}
}
};
context.on('fetch::end::*', onFetchEnd);
context.on('scan::end', onScanEnd);
}
}
exports.default = PerformanceBudgetHint;
PerformanceBudgetHint.meta = meta_1.default;