Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
257 lines
No EOL
12 KiB
Text
257 lines
No EOL
12 KiB
Text
// 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 * as Helpers from '../helpers/helpers.js';
|
|
import * as Types from '../types/types.js';
|
|
import { isRequestCompressed } from './Common.js';
|
|
import { InsightCategory, InsightKeys, InsightWarning, } 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',
|
|
};
|
|
// const str_ = i18n.i18n.registerUIStrings('models/trace/insights/DocumentLatency.ts', UIStrings);
|
|
export const i18nString = (i18nId, values) => ({i18nId, values}); // 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) {
|
|
return x.insightKey === 'DocumentLatency';
|
|
}
|
|
function getServerResponseTime(request, context) {
|
|
// 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;
|
|
}
|
|
const timing = request.args.data.timing;
|
|
if (!timing) {
|
|
return null;
|
|
}
|
|
const ms = Helpers.Timing.microToMilli(request.args.data.syntheticData.waiting);
|
|
return Math.round(ms);
|
|
}
|
|
function getCompressionSavings(request) {
|
|
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) {
|
|
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, context) {
|
|
if (!context.navigation) {
|
|
return finalize({});
|
|
}
|
|
const documentRequest = parsedTrace.NetworkRequests.byId.get(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,
|
|
LCP: overallSavingsMs,
|
|
};
|
|
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: (bytes => ({__i18nMillis: bytes}))(redirectDuration),
|
|
}),
|
|
value: noRedirects
|
|
},
|
|
serverResponseIsFast: {
|
|
label: serverResponseIsFast ?
|
|
i18nString(UIStrings.passingServerResponseTime, { PH1: (bytes => ({__i18nMillis: bytes}))(serverResponseTime) }) :
|
|
i18nString(UIStrings.failedServerResponseTime, { PH1: (bytes => ({__i18nMillis: bytes}))(serverResponseTime) }),
|
|
value: serverResponseIsFast
|
|
},
|
|
usesCompression: {
|
|
label: usesCompression ? i18nString(UIStrings.passingTextCompression) :
|
|
i18nString(UIStrings.failedTextCompression),
|
|
value: usesCompression
|
|
},
|
|
},
|
|
},
|
|
metricSavings,
|
|
wastedBytes: uncompressedResponseBytes,
|
|
});
|
|
}
|
|
export function createOverlays(model) {
|
|
if (!model.data?.documentRequest) {
|
|
return [];
|
|
}
|
|
const overlays = [];
|
|
const event = model.data.documentRequest;
|
|
const redirectDurationMicro = Helpers.Timing.milliToMicro(model.data.redirectDuration);
|
|
const sections = [];
|
|
if (model.data.redirectDuration) {
|
|
const bounds = Helpers.Timing.traceWindowFromMicroSeconds(event.ts, (event.ts + redirectDurationMicro));
|
|
sections.push({ bounds, label: i18nString(UIStrings.redirectsLabel), showDuration: true });
|
|
overlays.push({ type: 'CANDY_STRIPED_TIME_RANGE', bounds, entry: event });
|
|
}
|
|
if (!model.data.checklist.serverResponseIsFast.value) {
|
|
const serverResponseTimeMicro = Helpers.Timing.milliToMicro(model.data.serverResponseTime);
|
|
// NOTE: NetworkRequestHandlers never makes a synthetic network request event if `timing` is missing.
|
|
const sendEnd = event.args.data.timing?.sendEnd ?? Types.Timing.Milli(0);
|
|
const sendEndMicro = Helpers.Timing.milliToMicro(sendEnd);
|
|
const bounds = Helpers.Timing.traceWindowFromMicroSeconds(sendEndMicro, (sendEndMicro + serverResponseTimeMicro));
|
|
sections.push({ bounds, label: i18nString(UIStrings.serverResponseTimeLabel), showDuration: true });
|
|
}
|
|
if (model.data.uncompressedResponseBytes) {
|
|
const bounds = Helpers.Timing.traceWindowFromMicroSeconds(event.args.data.syntheticData.downloadStart, (event.args.data.syntheticData.downloadStart + event.args.data.syntheticData.download));
|
|
sections.push({ bounds, label: i18nString(UIStrings.uncompressedDownload), showDuration: true });
|
|
overlays.push({ type: 'CANDY_STRIPED_TIME_RANGE', bounds, entry: event });
|
|
}
|
|
if (sections.length) {
|
|
overlays.push({
|
|
type: 'TIMESPAN_BREAKDOWN',
|
|
sections,
|
|
entry: model.data.documentRequest,
|
|
// Always render below because the document request is guaranteed to be
|
|
// the first request in the network track.
|
|
renderLocation: 'BELOW_EVENT',
|
|
});
|
|
}
|
|
overlays.push({
|
|
type: 'ENTRY_SELECTED',
|
|
entry: model.data.documentRequest,
|
|
});
|
|
return overlays;
|
|
}
|
|
//# sourceMappingURL=DocumentLatency.js.map |