chrome-devtools-frontend
Version:
Chrome DevTools UI
238 lines (210 loc) • 9.18 kB
text/typescript
// Copyright 2024 The Chromium Authors
// 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 Handlers from '../handlers/handlers.js';
import * as Helpers from '../helpers/helpers.js';
import * as Types from '../types/types.js';
import {calculateDocFirstByteTs} 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 details about the LCP metric, and the network requests necessary to load it. Details how the LCP request was discoverable - in other words, the path necessary to load it (ex: network requests, JavaScript)
*/
title: 'LCP request discovery',
/**
* @description Description of an insight that provides details about the LCP metric, and the network requests necessary to load it.
*/
description:
'[Optimize LCP](https://developer.chrome.com/docs/performance/insights/lcp-discovery) by making the LCP image discoverable from the HTML immediately, and avoiding lazy-loading',
/**
* @description Text to tell the user how long after the earliest discovery time their LCP element loaded.
* @example {401ms} PH1
*/
lcpLoadDelay: 'LCP image loaded {PH1} after earliest start point.',
/**
* @description Text to tell the user that a fetchpriority property value of "high" is applied to the LCP request.
*/
fetchPriorityApplied: 'fetchpriority=high applied',
/**
* @description Text to tell the user that a fetchpriority property value of "high" should be applied to the LCP request.
*/
fetchPriorityShouldBeApplied: 'fetchpriority=high should be applied',
/**
* @description Text to tell the user that the LCP request is discoverable in the initial document.
*/
requestDiscoverable: 'Request is discoverable in initial document',
/**
* @description Text to tell the user that the LCP request does not have the lazy load property applied.
*/
lazyLoadNotApplied: 'lazy load not applied',
/**
* @description Text status indicating that the the Largest Contentful Paint (LCP) metric timing was not found. "LCP" is an acronym and should not be translated.
*/
noLcp: 'No LCP detected',
/**
* @description Text status indicating that the Largest Contentful Paint (LCP) metric was text rather than an image. "LCP" is an acronym and should not be translated.
*/
noLcpResource: 'No LCP resource detected because the LCP is not an image',
} as const;
const str_ = i18n.i18n.registerUIStrings('models/trace/insights/LCPDiscovery.ts', UIStrings);
export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export function isLCPDiscoveryInsight(model: InsightModel): model is LCPDiscoveryInsightModel {
return model.insightKey === 'LCPDiscovery';
}
export type LCPDiscoveryInsightModel = InsightModel<typeof UIStrings, {
lcpEvent?: Types.Events.LargestContentfulPaintCandidate,
/** The network request for the LCP image, if there was one. */
lcpRequest?: Types.Events.SyntheticNetworkRequest,
earliestDiscoveryTimeTs?: Types.Timing.Micro,
checklist?: Checklist<'priorityHinted'|'requestDiscoverable'|'eagerlyLoaded'>,
}>;
function finalize(partialModel: PartialInsightModel<LCPDiscoveryInsightModel>): LCPDiscoveryInsightModel {
const relatedEvents = partialModel.lcpEvent && partialModel.lcpRequest ?
// TODO: add entire request initiator chain?
[partialModel.lcpEvent, partialModel.lcpRequest] :
[];
return {
insightKey: InsightKeys.LCP_DISCOVERY,
strings: UIStrings,
title: i18nString(UIStrings.title),
description: i18nString(UIStrings.description),
docs: 'https://developer.chrome.com/docs/performance/insights/lcp-discovery',
category: InsightCategory.LCP,
state: partialModel.lcpRequest && partialModel.checklist &&
(!partialModel.checklist.eagerlyLoaded.value || !partialModel.checklist.requestDiscoverable.value ||
!partialModel.checklist.priorityHinted.value) ?
'fail' :
'pass',
...partialModel,
relatedEvents,
};
}
export function generateInsight(
data: Handlers.Types.HandlerData, context: InsightSetContext): LCPDiscoveryInsightModel {
if (!context.navigation) {
return finalize({});
}
const networkRequests = data.NetworkRequests;
const frameMetrics = data.PageLoadMetrics.metricScoresByFrameId.get(context.frameId);
if (!frameMetrics) {
throw new Error('no frame metrics');
}
const navMetrics = frameMetrics.get(context.navigationId);
if (!navMetrics) {
throw new Error('no navigation metrics');
}
const metricScore = navMetrics.get(Handlers.ModelHandlers.PageLoadMetrics.MetricName.LCP);
const lcpEvent = metricScore?.event;
if (!lcpEvent || !Types.Events.isLargestContentfulPaintCandidate(lcpEvent)) {
return finalize({warnings: [InsightWarning.NO_LCP]});
}
const docRequest = networkRequests.byId.get(context.navigationId);
if (!docRequest) {
return finalize({warnings: [InsightWarning.NO_DOCUMENT_REQUEST]});
}
const lcpRequest = data.LargestImagePaint.lcpRequestByNavigationId.get(context.navigationId);
if (!lcpRequest) {
return finalize({lcpEvent});
}
const initiatorUrl = lcpRequest.args.data.initiator?.url;
// TODO(b/372319476): Explore using trace event HTMLDocumentParser::FetchQueuedPreloads to determine if the request
// is discovered by the preload scanner.
const initiatedByMainDoc =
lcpRequest?.args.data.initiator?.type === 'parser' && docRequest.args.data.url === initiatorUrl;
const imgPreloadedOrFoundInHTML = lcpRequest?.args.data.isLinkPreload || initiatedByMainDoc;
const imageLoadingAttr = lcpEvent.args.data?.loadingAttr;
const imageFetchPriorityHint = lcpRequest?.args.data.fetchPriorityHint;
// This is the earliest discovery time an LCP request could have - it's TTFB (as an absolute timestamp).
const earliestDiscoveryTime = calculateDocFirstByteTs(docRequest);
const priorityHintFound = imageFetchPriorityHint === 'high';
return finalize({
lcpEvent,
lcpRequest,
earliestDiscoveryTimeTs: earliestDiscoveryTime ? Types.Timing.Micro(earliestDiscoveryTime) : undefined,
checklist: {
priorityHinted: {
label: priorityHintFound ? i18nString(UIStrings.fetchPriorityApplied) :
i18nString(UIStrings.fetchPriorityShouldBeApplied),
value: priorityHintFound
},
requestDiscoverable: {label: i18nString(UIStrings.requestDiscoverable), value: imgPreloadedOrFoundInHTML},
eagerlyLoaded: {label: i18nString(UIStrings.lazyLoadNotApplied), value: imageLoadingAttr !== 'lazy'},
},
});
}
interface LCPImageDiscoveryData {
checklist: Exclude<LCPDiscoveryInsightModel['checklist'], undefined>;
request: Types.Events.SyntheticNetworkRequest;
discoveryDelay: Types.Timing.Micro|null;
estimatedSavings: Types.Timing.Milli|null;
}
/**
* TODO: this extra transformation (getImageData) should not be necessary.
*/
export function getImageData(model: LCPDiscoveryInsightModel): LCPImageDiscoveryData|null {
if (!model.lcpRequest || !model.checklist) {
return null;
}
const shouldIncreasePriorityHint = !model.checklist.priorityHinted.value;
const shouldPreloadImage = !model.checklist.requestDiscoverable.value;
const shouldRemoveLazyLoading = !model.checklist.eagerlyLoaded.value;
const imageLCP = shouldIncreasePriorityHint !== undefined && shouldPreloadImage !== undefined &&
shouldRemoveLazyLoading !== undefined;
// Shouldn't render anything if lcp insight is null or lcp is text.
if (!imageLCP) {
return null;
}
const data: LCPImageDiscoveryData = {
checklist: model.checklist,
request: model.lcpRequest,
discoveryDelay: null,
estimatedSavings: model.metricSavings?.LCP ?? null,
};
if (model.earliestDiscoveryTimeTs && model.lcpRequest) {
const discoveryDelay = model.lcpRequest.ts - model.earliestDiscoveryTimeTs;
data.discoveryDelay = Types.Timing.Micro(discoveryDelay);
}
return data;
}
export function createOverlays(model: LCPDiscoveryInsightModel): Types.Overlays.Overlay[] {
const imageResults = getImageData(model);
if (!imageResults?.discoveryDelay) {
return [];
}
const delay = Helpers.Timing.traceWindowFromMicroSeconds(
Types.Timing.Micro(imageResults.request.ts - imageResults.discoveryDelay),
imageResults.request.ts,
);
return [
{
type: 'ENTRY_OUTLINE',
entry: imageResults.request,
outlineReason: 'ERROR',
},
{
type: 'CANDY_STRIPED_TIME_RANGE',
bounds: delay,
entry: imageResults.request,
},
{
type: 'TIMESPAN_BREAKDOWN',
sections: [{
bounds: delay,
// This is overridden in the component.
label: `${imageResults.discoveryDelay} microseconds`,
showDuration: false,
}],
entry: imageResults.request,
renderLocation: 'ABOVE_EVENT',
},
];
}