lighthouse
Version:
Automated auditing, performance metrics, and best practices for the web.
200 lines (171 loc) • 6.08 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {NO_NAVIGATION} from '@paulirish/trace_engine/models/trace/types/TraceEvents.js';
import {ProcessedTrace} from '../../computed/processed-trace.js';
import {TraceEngineResult} from '../../computed/trace-engine-result.js';
import {Audit} from '../audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
const str_ = i18n.createIcuMessageFn(import.meta.url, {});
/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<{insights: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet|undefined, parsedTrace: LH.Artifacts.TraceEngineResult['parsedTrace']}>}
*/
async function getInsightSet(artifacts, context) {
const settings = context.settings;
const trace = artifacts.Trace;
const processedTrace = await ProcessedTrace.request(trace, context);
const SourceMaps = artifacts.SourceMaps;
const traceEngineResult = await TraceEngineResult.request({trace, settings, SourceMaps}, context);
const navigationId = processedTrace.timeOriginEvt.args.data?.navigationId;
const key = navigationId ?? NO_NAVIGATION;
const insights = traceEngineResult.insights.get(key);
return {insights, parsedTrace: traceEngineResult.parsedTrace};
}
/**
* @typedef CreateDetailsExtras
* @property {import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet} insights
* @property {LH.Artifacts.TraceEngineResult['parsedTrace']} parsedTrace
*/
/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @param {T} insightName
* @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings: Array<string | LH.IcuMessage>}|LH.Audit.Details|undefined} createDetails
* @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
* @return {Promise<LH.Audit.Product>}
*/
async function adaptInsightToAuditProduct(artifacts, context, insightName, createDetails) {
const {insights, parsedTrace} = await getInsightSet(artifacts, context);
if (!insights) {
return {
scoreDisplayMode: Audit.SCORING_MODES.NOT_APPLICABLE,
score: null,
};
}
const insight = insights.model[insightName];
if (insight instanceof Error) {
return {
errorMessage: insight.message,
errorStack: insight.stack,
score: null,
};
}
const cbResult = createDetails(insight, {
parsedTrace,
insights,
});
const warnings = [...insight.warnings ?? []];
let details;
if (cbResult && 'warnings' in cbResult) {
details = cbResult.details;
warnings.push(...cbResult.warnings);
} else {
details = cbResult;
}
if (!details) {
return {
scoreDisplayMode: Audit.SCORING_MODES.NOT_APPLICABLE,
score: null,
details,
};
}
if (insight.wastedBytes !== undefined) {
if (!details.debugData) {
details.debugData = {type: 'debugdata'};
}
details.debugData.wastedBytes = insight.wastedBytes;
}
// TODO: FontDisplay insight (and maybe others) can return -Infinity savings when
// passing. That's weird. For now, just delete those.
if (insight.metricSavings) {
for (const [metric, value] of Object.entries(insight.metricSavings)) {
if (!Number.isFinite(value)) {
// @ts-expect-error
delete insight.metricSavings[metric];
}
}
}
// This hack is to add metric adorners if an insight category links it to a metric,
// but doesn't output a metric savings for that metric.
let metricSavings = insight.metricSavings;
if (insight.category === 'INP' && !metricSavings?.INP) {
metricSavings = {...metricSavings, INP: /** @type {any} */ (0)};
} else if (insight.category === 'CLS' && !metricSavings?.CLS) {
metricSavings = {...metricSavings, CLS: /** @type {any} */ (0)};
} else if (insight.category === 'LCP' && !metricSavings?.LCP) {
metricSavings = {...metricSavings, LCP: /** @type {any} */ (0)};
}
// TODO: consider adding a `estimatedSavingsText` to InsightModel, which can capture
// the exact i18n string used by RPP; and include the same est. timing savings.
let displayValue;
if (insight.wastedBytes) {
displayValue = str_(i18n.UIStrings.displayValueByteSavings, {wastedBytes: insight.wastedBytes});
} else {
let wastedMs;
switch (insight.insightKey) {
case 'DocumentLatency':
case 'DuplicatedJavaScript':
case 'FontDisplay':
case 'LegacyJavaScript':
case 'RenderBlocking': {
wastedMs = metricSavings?.FCP;
break;
}
case 'LCPDiscovery':
case 'ModernHTTP': {
wastedMs = metricSavings?.LCP;
break;
}
case 'Viewport': {
wastedMs = metricSavings?.INP;
break;
}
}
if (wastedMs) {
displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs});
}
}
let score;
let scoreDisplayMode;
if (insight.state === 'fail' || insight.state === 'pass') {
score = insight.state === 'fail' ? 0 : 1;
scoreDisplayMode =
insight.metricSavings ? Audit.SCORING_MODES.METRIC_SAVINGS : Audit.SCORING_MODES.NUMERIC;
} else {
score = null;
scoreDisplayMode = Audit.SCORING_MODES.INFORMATIVE;
}
return {
scoreDisplayMode,
score,
metricSavings,
warnings: warnings.length ? warnings : undefined,
displayValue,
details,
};
}
/**
* @param {LH.Artifacts.TraceElement[]} traceElements
* @param {number|null|undefined} nodeId
* @return {LH.Audit.Details.NodeValue|undefined}
*/
function makeNodeItemForNodeId(traceElements, nodeId) {
if (typeof nodeId !== 'number') {
return;
}
const traceElement =
traceElements.find(te => te.traceEventType === 'trace-engine' && te.nodeId === nodeId);
const node = traceElement?.node;
if (!node) {
return;
}
return Audit.makeNodeItem(node);
}
export {
adaptInsightToAuditProduct,
makeNodeItemForNodeId,
};