UNPKG

@hint/hint-performance-budget

Version:

hint that that checks if a page passes a set performance budget

160 lines (159 loc) 6.97 kB
"use strict"; 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;