UNPKG

chrome-devtools-frontend

Version:
241 lines (206 loc) • 9.23 kB
// Copyright 2025 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as i18n from '../../../core/i18n/i18n.js'; import * as Platform from '../../../core/platform/platform.js'; import * as Handlers from '../handlers/handlers.js'; import * as Helpers from '../helpers/helpers.js'; import type * as Lantern from '../lantern/lantern.js'; import type * as Types from '../types/types.js'; import { InsightCategory, InsightKeys, type InsightModel, type InsightSetContext, type MetricSavings, type PartialInsightModel, } from './types.js'; export const UIStrings = { /** * @description Title of an insight that recommends using HTTP/2 over HTTP/1.1 because of the performance benefits. "HTTP" should not be translated. */ title: 'Modern HTTP', /** * @description Description of an insight that recommends recommends using HTTP/2 over HTTP/1.1 because of the performance benefits. "HTTP" should not be translated. */ description: 'HTTP/2 and HTTP/3 offer many benefits over HTTP/1.1, such as multiplexing. [Learn more about using modern HTTP](https://developer.chrome.com/docs/lighthouse/best-practices/uses-http2/).', /** * @description Column header for a table where each cell represents a network request. */ request: 'Request', /** * @description Column header for a table where each cell represents the protocol of a network request. */ protocol: 'Protocol', /** * @description Text explaining that there were not requests that were slowed down by using HTTP/1.1. "HTTP/1.1" should not be translated. */ noOldProtocolRequests: 'No requests used HTTP/1.1' } as const; const str_ = i18n.i18n.registerUIStrings('models/trace/insights/ModernHTTP.ts', UIStrings); export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export type UseModernHTTPInsightModel = InsightModel<typeof UIStrings, { requests: Types.Events.SyntheticNetworkRequest[], }>; /** * 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. */ function isMultiplexableStaticAsset( request: Types.Events.SyntheticNetworkRequest, entityMappings: Handlers.Helpers.EntityMappings, firstPartyEntity: Handlers.Helpers.Entity|null): boolean { if (!Helpers.Network.STATIC_RESOURCE_TYPES.has(request.args.data.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 (request.args.data.decodedBodyLength < 100) { const entity = entityMappings.entityByEvent.get(request); if (entity) { // Third-party assets are multiplexable in their first-party context. if (firstPartyEntity?.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. [1][2][3] * - 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) * * [1] https://news.ycombinator.com/item?id=19086639 * [2] https://www.twilio.com/blog/2017/10/http2-issues.html * [3] https://www.cachefly.com/http-2-is-not-a-magic-bullet/ */ export function determineNonHttp2Resources( requests: Types.Events.SyntheticNetworkRequest[], entityMappings: Handlers.Helpers.EntityMappings, firstPartyEntity: Handlers.Helpers.Entity|null): Types.Events.SyntheticNetworkRequest[] { const nonHttp2Resources: Types.Events.SyntheticNetworkRequest[] = []; const groupedByOrigin = new Map<string, Types.Events.SyntheticNetworkRequest[]>(); for (const record of requests) { const url = new URL(record.args.data.url); if (!isMultiplexableStaticAsset(record, entityMappings, firstPartyEntity)) { continue; } if (Helpers.Network.isSyntheticNetworkRequestLocalhost(record)) { continue; } const originRequests = Platform.MapUtilities.getWithDefault(groupedByOrigin, url.origin, () => []); originRequests.push(record); } const seenURLs = new Set<string>(); for (const request of requests) { // Skip duplicates. if (seenURLs.has(request.args.data.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 (request.args.data.fromServiceWorker) { continue; } // Test the protocol to see if it was http/1.1. const isOldHttp = /HTTP\/[01][.\d]?/i.test(request.args.data.protocol); if (!isOldHttp) { continue; } const url = new URL(request.args.data.url); // Check if the origin has enough requests to bother flagging. const group = groupedByOrigin.get(url.origin) || []; if (group.length < 6) { continue; } seenURLs.add(request.args.data.url); nonHttp2Resources.push(request); } return nonHttp2Resources; } /** * Computes the estimated effect of all results being converted to http/2 on the provided graph. */ function computeWasteWithGraph( urlsToChange: Set<string>, graph: Lantern.Graph.Node, simulator: Lantern.Simulation.Simulator): Types.Timing.Milli { const simulationBefore = simulator.simulate(graph); // Update all the protocols to reflect implementing our recommendations 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); // 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 Platform.NumberUtilities.floor(savings, 1 / 10) as Types.Timing.Milli; } function computeMetricSavings( nonHttp2Requests: Types.Events.SyntheticNetworkRequest[], context: InsightSetContext): MetricSavings|undefined { if (!context.navigation || !context.lantern) { return; } const urlsToChange = new Set(nonHttp2Requests.map(r => r.args.data.url)); const fcpGraph = context.lantern.metrics.firstContentfulPaint.optimisticGraph; const lcpGraph = context.lantern.metrics.largestContentfulPaint.optimisticGraph; return { FCP: computeWasteWithGraph(urlsToChange, fcpGraph, context.lantern.simulator), LCP: computeWasteWithGraph(urlsToChange, lcpGraph, context.lantern.simulator), }; } function finalize(partialModel: PartialInsightModel<UseModernHTTPInsightModel>): UseModernHTTPInsightModel { return { insightKey: InsightKeys.IMAGE_DELIVERY, strings: UIStrings, title: i18nString(UIStrings.title), description: i18nString(UIStrings.description), category: InsightCategory.LCP, state: partialModel.requests.length > 0 ? 'fail' : 'pass', ...partialModel, relatedEvents: partialModel.requests, }; } export function generateInsight( parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): UseModernHTTPInsightModel { const isWithinContext = (event: Types.Events.Event): boolean => Helpers.Timing.eventIsInBounds(event, context.bounds); const contextRequests = parsedTrace.NetworkRequests.byTime.filter(isWithinContext); const entityMappings = parsedTrace.NetworkRequests.entityMappings; const firstPartyUrl = context.navigation?.args.data?.documentLoaderURL ?? parsedTrace.Meta.mainFrameURL; const firstPartyEntity = Handlers.Helpers.getEntityForUrl(firstPartyUrl, entityMappings.createdEntityCache); const nonHttp2Requests = determineNonHttp2Resources(contextRequests, entityMappings, firstPartyEntity ?? null); return finalize({ requests: nonHttp2Requests, metricSavings: computeMetricSavings(nonHttp2Requests, context), }); }