UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

277 lines (242 loc) • 11.3 kB
/** * @license * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Audit a page to ensure that resource loaded over its own * origin are over the http/2 protocol. */ import {Audit} from '../audit.js'; import {EntityClassification} from '../../computed/entity-classification.js'; import UrlUtils from '../../lib/url-utils.js'; import {NetworkRequest} from '../../lib/network-request.js'; import {NetworkRecords} from '../../computed/network-records.js'; import {LoadSimulator} from '../../computed/load-simulator.js'; import {LanternLargestContentfulPaint} from '../../computed/metrics/lantern-largest-contentful-paint.js'; import {LanternFirstContentfulPaint} from '../../computed/metrics/lantern-first-contentful-paint.js'; import * as i18n from '../../lib/i18n/i18n.js'; const UIStrings = { /** Imperative title of a Lighthouse audit that tells the user to enable HTTP/2. This is displayed in a list of audit titles that Lighthouse generates. */ title: 'Use HTTP/2', /** Description of a Lighthouse audit that tells the user why they should use HTTP/2. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */ description: 'HTTP/2 offers many benefits over HTTP/1.1, including binary headers and ' + 'multiplexing. [Learn more about HTTP/2](https://developer.chrome.com/docs/lighthouse/best-practices/uses-http2/).', /** [ICU Syntax] Label identifying the number of network requests that were not served with HTTP/2. */ displayValue: `{itemCount, plural, =1 {1 request not served via HTTP/2} other {# requests not served via HTTP/2} }`, /** Label for a column in a data table; entries in the column will be the HTTP Protocol used to make a network request. */ columnProtocol: 'Protocol', }; const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); /** @type {Set<LH.Artifacts.NetworkRequest['resourceType']>} */ const STATIC_RESOURCE_TYPES = new Set([ NetworkRequest.TYPES.Document, NetworkRequest.TYPES.Font, NetworkRequest.TYPES.Image, NetworkRequest.TYPES.Stylesheet, NetworkRequest.TYPES.Script, NetworkRequest.TYPES.Media, ]); class UsesHTTP2Audit extends Audit { /** * @return {LH.Audit.Meta} */ static get meta() { return { id: 'uses-http2', title: str_(UIStrings.title), description: str_(UIStrings.description), scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS, guidanceLevel: 3, supportedModes: ['timespan', 'navigation'], requiredArtifacts: ['URL', 'DevtoolsLog', 'Trace', 'GatherContext', 'SourceMaps'], }; } /** * Computes the estimated effect of all results being converted to http/2 on the provided graph. * * @param {Array<{url: string}>} results * @param {LH.Gatherer.Simulation.GraphNode} graph * @param {LH.Gatherer.Simulation.Simulator} simulator * @param {{label?: string}=} options * @return {{savings: number, simulationBefore: LH.Gatherer.Simulation.Result, simulationAfter: LH.Gatherer.Simulation.Result}} */ static computeWasteWithGraph(results, graph, simulator, options) { options = Object.assign({label: ''}, options); const beforeLabel = `${this.meta.id}-${options.label}-before`; const afterLabel = `${this.meta.id}-${options.label}-after`; const urlsToChange = new Set(results.map(result => result.url)); const simulationBefore = simulator.simulate(graph, {label: beforeLabel}); // Update all the protocols to reflect implementing our recommendations /** @type {Map<string, string>} */ const originalProtocols = new Map(); graph.traverse(node => { if (node.type !== 'network') return; if (!urlsToChange.has(node.request.url)) return; originalProtocols.set(node.request.requestId, node.request.protocol); node.request.protocol = 'h2'; }); const simulationAfter = simulator.simulate(graph, {label: afterLabel}); // Restore the original protocol after we've done our simulation graph.traverse(node => { if (node.type !== 'network') return; const originalProtocol = originalProtocols.get(node.request.requestId); if (originalProtocol === undefined) return; node.request.protocol = originalProtocol; }); const savings = simulationBefore.timeInMs - simulationAfter.timeInMs; return { // Round waste to nearest 10ms savings: Math.round(Math.max(savings, 0) / 10) * 10, simulationBefore, simulationAfter, }; } /** * Determines whether a network request is a "static resource" that would benefit from H2 multiplexing. * XHRs, tracking pixels, etc generally don't benefit as much because they aren't requested en-masse * for the same origin at the exact same time. * * @param {LH.Artifacts.NetworkRequest} networkRequest * @param {LH.Artifacts.EntityClassification} classifiedEntities * @return {boolean} */ static isMultiplexableStaticAsset(networkRequest, classifiedEntities) { if (!STATIC_RESOURCE_TYPES.has(networkRequest.resourceType)) return false; // Resources from third-parties that are less than 100 bytes are usually tracking pixels, not actual resources. // They can masquerade as static types though (gifs, documents, etc) if (networkRequest.resourceSize < 100) { const entity = classifiedEntities.entityByUrl.get(networkRequest.url); if (entity) { // Third-party assets are multiplexable in their first-party context. if (classifiedEntities.firstParty?.name === entity.name) return true; // Skip recognizable third-parties' requests. if (!entity.isUnrecognized) return false; } } return true; } /** * Determine the set of resources that aren't HTTP/2 but should be. * We're a little conservative about what we surface for a few reasons: * * - The simulator approximation of HTTP/2 is a little more generous than reality. * - There's a bit of debate surrounding HTTP/2 due to its worse performance in environments with high packet loss.** * - It's something that you'd have absolutely zero control over with a third-party (can't defer to fix it for example). * * Therefore, we only surface requests that were... * * - Served over HTTP/1.1 or earlier * - Served over an origin that serves at least 6 static asset requests * (if there aren't more requests than browser's max/host, multiplexing isn't as big a deal) * - Not served on localhost (h2 is a pain to deal with locally & and CI) * * ** = https://news.ycombinator.com/item?id=19086639 * https://www.twilio.com/blog/2017/10/http2-issues.html * https://www.cachefly.com/http-2-is-not-a-magic-bullet/ * * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords * @param {LH.Artifacts.EntityClassification} classifiedEntities * @return {Array<{url: string, protocol: string}>} */ static determineNonHttp2Resources(networkRecords, classifiedEntities) { /** @type {Array<{url: string, protocol: string}>} */ const nonHttp2Resources = []; /** @type {Set<string>} */ const seenURLs = new Set(); /** @type {Map<string, Array<LH.Artifacts.NetworkRequest>>} */ const groupedByOrigin = new Map(); for (const record of networkRecords) { if (!UsesHTTP2Audit.isMultiplexableStaticAsset(record, classifiedEntities)) continue; if (UrlUtils.isLikeLocalhost(record.parsedURL.host)) continue; const existing = groupedByOrigin.get(record.parsedURL.securityOrigin) || []; existing.push(record); groupedByOrigin.set(record.parsedURL.securityOrigin, existing); } for (const record of networkRecords) { // Skip duplicates. if (seenURLs.has(record.url)) continue; // Check if record is not served through the service worker, servicer worker uses http/1.1 as a protocol. // These can generate false positives (bug: https://github.com/GoogleChrome/lighthouse/issues/7158). if (record.fetchedViaServiceWorker) continue; // Test the protocol to see if it was http/1.1. const isOldHttp = /HTTP\/[01][.\d]?/i.test(record.protocol); if (!isOldHttp) continue; // Check if the origin has enough requests to bother flagging. const group = groupedByOrigin.get(record.parsedURL.securityOrigin) || []; if (group.length < 6) continue; seenURLs.add(record.url); nonHttp2Resources.push({protocol: record.protocol, url: record.url}); } return nonHttp2Resources; } /** * @param {LH.Artifacts} artifacts * @param {LH.Audit.Context} context * @return {Promise<LH.Audit.Product>} */ static async audit(artifacts, context) { const devtoolsLog = artifacts.DevtoolsLog; const URL = artifacts.URL; const networkRecords = await NetworkRecords.request(devtoolsLog, context); const classifiedEntities = await EntityClassification.request({URL, devtoolsLog}, context); const resources = UsesHTTP2Audit.determineNonHttp2Resources(networkRecords, classifiedEntities); let displayValue; if (resources.length > 0) { displayValue = str_(UIStrings.displayValue, {itemCount: resources.length}); } // TODO: Compute actual savings for timespan mode. if (artifacts.GatherContext.gatherMode === 'timespan') { /** @type {LH.Audit.Details.Table['headings']} */ const headings = [ {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)}, {key: 'protocol', valueType: 'text', label: str_(UIStrings.columnProtocol)}, ]; const details = Audit.makeTableDetails(headings, resources); return { displayValue, score: resources.length ? 0 : 1, details, }; } const settings = context?.settings || {}; const simulatorOptions = { devtoolsLog, settings, }; const simulator = await LoadSimulator.request(simulatorOptions, context); const metricComputationInput = Audit.makeMetricComputationDataInput(artifacts, context); const { pessimisticGraph: fcpGraph, } = await LanternFirstContentfulPaint.request(metricComputationInput, context); const { pessimisticGraph: lcpGraph, } = await LanternLargestContentfulPaint.request(metricComputationInput, context); const wasteFcp = UsesHTTP2Audit.computeWasteWithGraph(resources, fcpGraph, simulator, {label: 'fcp'}); const wasteLcp = UsesHTTP2Audit.computeWasteWithGraph(resources, lcpGraph, simulator, {label: 'lcp'}); /** @type {LH.Audit.Details.Opportunity['headings']} */ const headings = [ {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)}, {key: 'protocol', valueType: 'text', label: str_(UIStrings.columnProtocol)}, ]; const details = Audit.makeOpportunityDetails(headings, resources, {overallSavingsMs: wasteLcp.savings}); return { displayValue, numericValue: wasteLcp.savings, numericUnit: 'millisecond', score: resources.length ? 0 : 1, details, metricSavings: {LCP: wasteLcp.savings, FCP: wasteFcp.savings}, }; } } export default UsesHTTP2Audit; export {UIStrings};