UNPKG

chrome-devtools-frontend

Version:
264 lines (238 loc) • 10.2 kB
// Copyright 2024 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 type * as Handlers from '../handlers/handlers.js'; import * as Helpers from '../helpers/helpers.js'; import * as Types from '../types/types.js'; import {isRequestCompressed} from './Common.js'; import { type Checklist, InsightCategory, InsightKeys, type InsightModel, type InsightSetContext, InsightWarning, type PartialInsightModel, } from './types.js'; export const UIStrings = { /** *@description Title of an insight that provides a breakdown for how long it took to download the main document. */ title: 'Document request latency', /** *@description Description of an insight that provides a breakdown for how long it took to download the main document. */ description: 'Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression.', /** * @description Text to tell the user that the document request does not have redirects. */ passingRedirects: 'Avoids redirects', /** * @description Text to tell the user that the document request had redirects. * @example {3} PH1 * @example {1000 ms} PH2 */ failedRedirects: 'Had redirects ({PH1} redirects, +{PH2})', /** * @description Text to tell the user that the time starting the document request to when the server started responding is acceptable. * @example {600 ms} PH1 */ passingServerResponseTime: 'Server responds quickly (observed {PH1})', /** * @description Text to tell the user that the time starting the document request to when the server started responding is not acceptable. * @example {601 ms} PH1 */ failedServerResponseTime: 'Server responded slowly (observed {PH1})', /** * @description Text to tell the user that text compression (like gzip) was applied. */ passingTextCompression: 'Applies text compression', /** * @description Text to tell the user that text compression (like gzip) was not applied. */ failedTextCompression: 'No compression applied', /** * @description Text for a label describing a network request event as having redirects. */ redirectsLabel: 'Redirects', /** * @description Text for a label describing a network request event as taking too long to start delivery by the server. */ serverResponseTimeLabel: 'Server response time', /** * @description Text for a label describing a network request event as taking longer to download because it wasn't compressed. */ uncompressedDownload: 'Uncompressed download', } as const; const str_ = i18n.i18n.registerUIStrings('models/trace/insights/DocumentLatency.ts', UIStrings); export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); // Due to the way that DevTools throttling works we cannot see if server response took less than ~570ms. // We set our failure threshold to 600ms to avoid those false positives but we want devs to shoot for 100ms. const TOO_SLOW_THRESHOLD_MS = 600; const TARGET_MS = 100; // Threshold for compression savings. const IGNORE_THRESHOLD_IN_BYTES = 1400; export function isDocumentLatency(x: InsightModel): x is DocumentLatencyInsightModel { return x.insightKey === 'DocumentLatency'; } export type DocumentLatencyInsightModel = InsightModel<typeof UIStrings, { data?: { serverResponseTime: Types.Timing.Milli, redirectDuration: Types.Timing.Milli, uncompressedResponseBytes: number, checklist: Checklist<'noRedirects'|'serverResponseIsFast'|'usesCompression'>, documentRequest?: Types.Events.SyntheticNetworkRequest, }, }>; function getServerResponseTime( request: Types.Events.SyntheticNetworkRequest, context: InsightSetContext): Types.Timing.Milli|null { // Prefer the value as given by the Lantern provider. // For PSI, Lighthouse uses this to set a better value for the server response // time. For technical reasons, in Lightrider we do not have `sendEnd` timing // values. See Lighthouse's `asLanternNetworkRequest` function for more. const lanternRequest = context.navigation && context.lantern?.requests.find(r => r.rawRequest === request); if (lanternRequest?.serverResponseTime !== undefined) { return lanternRequest.serverResponseTime as Types.Timing.Milli; } const timing = request.args.data.timing; if (!timing) { return null; } const ms = Helpers.Timing.microToMilli(request.args.data.syntheticData.waiting); return Math.round(ms) as Types.Timing.Milli; } function getCompressionSavings(request: Types.Events.SyntheticNetworkRequest): number { const isCompressed = isRequestCompressed(request); if (isCompressed) { return 0; } // We don't know how many bytes this asset used on the network, but we can guess it was // roughly the size of the content gzipped. // See https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/optimize-encoding-and-transfer for specific CSS/Script examples // See https://discuss.httparchive.org/t/file-size-and-compression-savings/145 for fallback multipliers // See https://letstalkaboutwebperf.com/en/gzip-brotli-server-config/ for MIME types to compress const originalSize = request.args.data.decodedBodyLength; let estimatedSavings = 0; switch (request.args.data.mimeType) { case 'text/css': // Stylesheets tend to compress extremely well. estimatedSavings = Math.round(originalSize * 0.8); break; case 'text/html': case 'text/javascript': // Scripts and HTML compress fairly well too. estimatedSavings = Math.round(originalSize * 0.67); break; case 'text/plain': case 'text/xml': case 'text/x-component': case 'application/javascript': case 'application/json': case 'application/manifest+json': case 'application/vnd.api+json': case 'application/xml': case 'application/xhtml+xml': case 'application/rss+xml': case 'application/atom+xml': case 'application/vnd.ms-fontobject': case 'application/x-font-ttf': case 'application/x-font-opentype': case 'application/x-font-truetype': case 'image/svg+xml': case 'image/x-icon': case 'image/vnd.microsoft.icon': case 'font/ttf': case 'font/eot': case 'font/otf': case 'font/opentype': // Use the average savings in HTTPArchive. estimatedSavings = Math.round(originalSize * 0.5); break; default: // Any other MIME types are likely already compressed. } // Check if the estimated savings are greater than the byte ignore threshold. // Note that the estimated gzip savings are always more than 10%, so there is // no percent threshold. return estimatedSavings < IGNORE_THRESHOLD_IN_BYTES ? 0 : estimatedSavings; } function finalize(partialModel: PartialInsightModel<DocumentLatencyInsightModel>): DocumentLatencyInsightModel { let hasFailure = false; if (partialModel.data) { hasFailure = !partialModel.data.checklist.usesCompression.value || !partialModel.data.checklist.serverResponseIsFast.value || !partialModel.data.checklist.noRedirects.value; } return { insightKey: InsightKeys.DOCUMENT_LATENCY, strings: UIStrings, title: i18nString(UIStrings.title), description: i18nString(UIStrings.description), category: InsightCategory.ALL, state: hasFailure ? 'fail' : 'pass', ...partialModel, }; } export function generateInsight( parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): DocumentLatencyInsightModel { if (!context.navigation) { return finalize({}); } const documentRequest = parsedTrace.NetworkRequests.byTime.find(req => req.args.data.requestId === context.navigationId); if (!documentRequest) { return finalize({warnings: [InsightWarning.NO_DOCUMENT_REQUEST]}); } const serverResponseTime = getServerResponseTime(documentRequest, context); if (serverResponseTime === null) { throw new Error('missing document request timing'); } const serverResponseTooSlow = serverResponseTime > TOO_SLOW_THRESHOLD_MS; let overallSavingsMs = 0; if (serverResponseTime > TOO_SLOW_THRESHOLD_MS) { overallSavingsMs = Math.max(serverResponseTime - TARGET_MS, 0); } const redirectDuration = Math.round(documentRequest.args.data.syntheticData.redirectionDuration / 1000); overallSavingsMs += redirectDuration; const metricSavings = { FCP: overallSavingsMs as Types.Timing.Milli, LCP: overallSavingsMs as Types.Timing.Milli, }; const uncompressedResponseBytes = getCompressionSavings(documentRequest); const noRedirects = redirectDuration === 0; const serverResponseIsFast = !serverResponseTooSlow; const usesCompression = uncompressedResponseBytes === 0; return finalize({ relatedEvents: [documentRequest], data: { serverResponseTime, redirectDuration: Types.Timing.Milli(redirectDuration), uncompressedResponseBytes, documentRequest, checklist: { noRedirects: { label: noRedirects ? i18nString(UIStrings.passingRedirects) : i18nString(UIStrings.failedRedirects, { PH1: documentRequest.args.data.redirects.length, PH2: i18n.TimeUtilities.millisToString(redirectDuration), }), value: noRedirects }, serverResponseIsFast: { label: serverResponseIsFast ? i18nString( UIStrings.passingServerResponseTime, {PH1: i18n.TimeUtilities.millisToString(serverResponseTime)}) : i18nString( UIStrings.failedServerResponseTime, {PH1: i18n.TimeUtilities.millisToString(serverResponseTime)}), value: serverResponseIsFast }, usesCompression: { label: usesCompression ? i18nString(UIStrings.passingTextCompression) : i18nString(UIStrings.failedTextCompression), value: usesCompression }, }, }, metricSavings, wastedBytes: uncompressedResponseBytes, }); }