lighthouse 12.3.0-dev.20250208 → 12.3.0-dev.20250210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/audits/bootup-time.js +0 -2
- package/core/audits/byte-efficiency/total-byte-weight.js +0 -2
- package/core/audits/dobetterweb/dom-size.js +0 -2
- package/core/audits/insights/image-delivery-insight.js +31 -6
- package/core/audits/insights/insight-audit.d.ts +7 -0
- package/core/audits/insights/insight-audit.js +35 -1
- package/core/audits/insights/interaction-to-next-paint-insight.js +23 -5
- package/core/audits/insights/lcp-phases-insight.d.ts +5 -0
- package/core/audits/insights/lcp-phases-insight.js +45 -11
- package/core/audits/insights/third-parties-insight.d.ts +17 -0
- package/core/audits/insights/third-parties-insight.js +44 -7
- package/core/audits/mainthread-work-breakdown.js +0 -2
- package/core/audits/seo/is-crawlable.d.ts +1 -0
- package/core/audits/server-response-time.js +0 -1
- package/core/computed/trace-engine-result.d.ts +4 -0
- package/core/computed/trace-engine-result.js +88 -0
- package/core/config/default-config.js +14 -14
- package/core/gather/gatherers/trace-elements.js +1 -1
- package/core/lib/trace-engine.d.ts +1 -0
- package/core/lib/trace-engine.js +2 -0
- package/dist/report/bundle.esm.js +13 -10
- package/dist/report/flow.js +8 -5
- package/dist/report/standalone.js +7 -4
- package/flow-report/src/i18n/i18n.d.ts +2 -0
- package/package.json +2 -2
- package/report/assets/styles.css +3 -0
- package/report/assets/templates.html +1 -0
- package/report/renderer/components.js +8 -2
- package/report/renderer/performance-category-renderer.d.ts +10 -0
- package/report/renderer/performance-category-renderer.js +34 -23
- package/report/renderer/report-utils.d.ts +1 -0
- package/report/renderer/report-utils.js +2 -0
- package/report/renderer/topbar-features.js +8 -0
- package/shared/localization/locales/en-US.json +8 -2
- package/shared/localization/locales/en-XL.json +8 -2
- package/types/lhr/audit-details.d.ts +2 -0
|
@@ -14,7 +14,6 @@ import {MainThreadTasks} from '../computed/main-thread-tasks.js';
|
|
|
14
14
|
import {getExecutionTimingsByURL} from '../lib/tracehouse/task-summary.js';
|
|
15
15
|
import {TBTImpactTasks} from '../computed/tbt-impact-tasks.js';
|
|
16
16
|
import {Sentry} from '../lib/sentry.js';
|
|
17
|
-
import {Util} from '../../shared/util.js';
|
|
18
17
|
|
|
19
18
|
const UIStrings = {
|
|
20
19
|
/** Title of a diagnostic audit that provides detail on the time spent executing javascript files during the load. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -173,7 +172,6 @@ class BootupTime extends Audit {
|
|
|
173
172
|
|
|
174
173
|
return {
|
|
175
174
|
score,
|
|
176
|
-
scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
177
175
|
notApplicable: !results.length,
|
|
178
176
|
numericValue: totalBootupTime,
|
|
179
177
|
numericUnit: 'millisecond',
|
|
@@ -8,7 +8,6 @@ import {Audit} from '../audit.js';
|
|
|
8
8
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
9
9
|
import {NetworkRequest} from '../../lib/network-request.js';
|
|
10
10
|
import {NetworkRecords} from '../../computed/network-records.js';
|
|
11
|
-
import {Util} from '../../../shared/util.js';
|
|
12
11
|
|
|
13
12
|
const UIStrings = {
|
|
14
13
|
/** Title of a diagnostic audit that provides detail on large network resources required during page load. 'Payloads' is roughly equivalent to 'resources'. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -99,7 +98,6 @@ class TotalByteWeight extends Audit {
|
|
|
99
98
|
|
|
100
99
|
return {
|
|
101
100
|
score,
|
|
102
|
-
scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
103
101
|
numericValue: totalBytes,
|
|
104
102
|
numericUnit: 'byte',
|
|
105
103
|
displayValue: str_(UIStrings.displayValue, {totalBytes}),
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import {Audit} from '../audit.js';
|
|
15
15
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
16
16
|
import {TBTImpactTasks} from '../../computed/tbt-impact-tasks.js';
|
|
17
|
-
import {Util} from '../../../shared/util.js';
|
|
18
17
|
|
|
19
18
|
const UIStrings = {
|
|
20
19
|
/** Title of a diagnostic audit that provides detail on the size of the web page's DOM. The size of a DOM is characterized by the total number of DOM elements and greatest DOM depth. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -168,7 +167,6 @@ class DOMSize extends Audit {
|
|
|
168
167
|
|
|
169
168
|
return {
|
|
170
169
|
score,
|
|
171
|
-
scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
172
170
|
numericValue: stats.totalBodyElements,
|
|
173
171
|
numericUnit: 'element',
|
|
174
172
|
displayValue: str_(UIStrings.displayValue, {itemCount: stats.totalBodyElements}),
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @license
|
|
5
3
|
* Copyright 2025 Google LLC
|
|
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/ImageDeli
|
|
|
10
8
|
|
|
11
9
|
import {Audit} from '../audit.js';
|
|
12
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
13
|
-
import {adaptInsightToAuditProduct
|
|
11
|
+
import {adaptInsightToAuditProduct} from './insight-audit.js';
|
|
14
12
|
|
|
15
13
|
// eslint-disable-next-line max-len
|
|
16
14
|
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/ImageDelivery.js', UIStrings);
|
|
@@ -36,14 +34,41 @@ class ImageDeliveryInsight extends Audit {
|
|
|
36
34
|
* @return {Promise<LH.Audit.Product>}
|
|
37
35
|
*/
|
|
38
36
|
static async audit(artifacts, context) {
|
|
39
|
-
// TODO: implement.
|
|
40
37
|
return adaptInsightToAuditProduct(artifacts, context, 'ImageDelivery', (insight) => {
|
|
38
|
+
if (!insight.optimizableImages.length) {
|
|
39
|
+
// TODO: show UIStrings.noOptimizableImages?
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const relatedEventsMap = insight.relatedEvents && !Array.isArray(insight.relatedEvents) ?
|
|
44
|
+
insight.relatedEvents :
|
|
45
|
+
null;
|
|
46
|
+
|
|
41
47
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
42
48
|
const headings = [
|
|
49
|
+
/* eslint-disable max-len */
|
|
50
|
+
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL), subItemsHeading: {key: 'reason', valueType: 'text'}},
|
|
51
|
+
{key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
|
|
52
|
+
{key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes), subItemsHeading: {key: 'wastedBytes', valueType: 'bytes'}},
|
|
53
|
+
/* eslint-enable max-len */
|
|
43
54
|
];
|
|
55
|
+
|
|
44
56
|
/** @type {LH.Audit.Details.Table['items']} */
|
|
45
|
-
const items =
|
|
46
|
-
|
|
57
|
+
const items = insight.optimizableImages.map(image => ({
|
|
58
|
+
url: image.request.args.data.url,
|
|
59
|
+
totalBytes: image.request.args.data.decodedBodyLength,
|
|
60
|
+
wastedBytes: image.byteSavings,
|
|
61
|
+
subItems: {
|
|
62
|
+
type: /** @type {const} */ ('subitems'),
|
|
63
|
+
// TODO: when strings update to remove number from "reason" uistrings, update this
|
|
64
|
+
// to use `image.optimizations.map(...)` and construct strings from the type.
|
|
65
|
+
items: (relatedEventsMap?.get(image.request) ?? []).map((reason, i) => ({
|
|
66
|
+
reason,
|
|
67
|
+
wastedBytes: image.optimizations[i].byteSavings,
|
|
68
|
+
})),
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
71
|
+
|
|
47
72
|
return Audit.makeTableDetails(headings, items);
|
|
48
73
|
});
|
|
49
74
|
}
|
|
@@ -13,4 +13,11 @@ export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/tr
|
|
|
13
13
|
* @return {LH.Audit.Details.NodeValue|undefined}
|
|
14
14
|
*/
|
|
15
15
|
export function makeNodeItemForNodeId(traceElements: LH.Artifacts.TraceElement[], nodeId: number | null | undefined): LH.Audit.Details.NodeValue | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* @param {LH.Artifacts.TraceElement[]} traceElements
|
|
18
|
+
* @param {number|null|undefined} nodeId
|
|
19
|
+
* @param {LH.IcuMessage|string} label
|
|
20
|
+
* @return {LH.Audit.Details.Table|undefined}
|
|
21
|
+
*/
|
|
22
|
+
export function maybeMakeNodeElementTable(traceElements: LH.Artifacts.TraceElement[], nodeId: number | null | undefined, label: LH.IcuMessage | string): LH.Audit.Details.Table | undefined;
|
|
16
23
|
//# sourceMappingURL=insight-audit.d.ts.map
|
|
@@ -44,6 +44,14 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const insight = insights.model[insightName];
|
|
47
|
+
if (insight instanceof Error) {
|
|
48
|
+
return {
|
|
49
|
+
errorMessage: insight.message,
|
|
50
|
+
errorStack: insight.stack,
|
|
51
|
+
score: null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
47
55
|
const details = createDetails(insight);
|
|
48
56
|
if (!details || (details.type === 'table' && details.headings.length === 0)) {
|
|
49
57
|
return {
|
|
@@ -63,10 +71,18 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
|
|
|
63
71
|
metricSavings = {...metricSavings, LCP: /** @type {any} */ (0)};
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
let score = insight.shouldShow ? 0 : 1;
|
|
75
|
+
// TODO: change insight model to denote passing/failing/informative. Until then... hack it.
|
|
76
|
+
if (insightName === 'LCPPhases') {
|
|
77
|
+
score = metricSavings?.LCP ?? 0 >= 1000 ? 0 : 1;
|
|
78
|
+
} else if (insightName === 'InteractionToNextPaint') {
|
|
79
|
+
score = metricSavings?.INP ?? 0 >= 500 ? 0 : 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
66
82
|
return {
|
|
67
83
|
scoreDisplayMode:
|
|
68
84
|
insight.metricSavings ? Audit.SCORING_MODES.METRIC_SAVINGS : Audit.SCORING_MODES.NUMERIC,
|
|
69
|
-
score
|
|
85
|
+
score,
|
|
70
86
|
metricSavings,
|
|
71
87
|
warnings: insight.warnings,
|
|
72
88
|
details,
|
|
@@ -93,7 +109,25 @@ function makeNodeItemForNodeId(traceElements, nodeId) {
|
|
|
93
109
|
return Audit.makeNodeItem(node);
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
/**
|
|
113
|
+
* @param {LH.Artifacts.TraceElement[]} traceElements
|
|
114
|
+
* @param {number|null|undefined} nodeId
|
|
115
|
+
* @param {LH.IcuMessage|string} label
|
|
116
|
+
* @return {LH.Audit.Details.Table|undefined}
|
|
117
|
+
*/
|
|
118
|
+
function maybeMakeNodeElementTable(traceElements, nodeId, label) {
|
|
119
|
+
const node = makeNodeItemForNodeId(traceElements, nodeId);
|
|
120
|
+
if (!node) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Audit.makeTableDetails([
|
|
125
|
+
{key: 'node', valueType: 'node', label},
|
|
126
|
+
], [{node}]);
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
export {
|
|
97
130
|
adaptInsightToAuditProduct,
|
|
98
131
|
makeNodeItemForNodeId,
|
|
132
|
+
maybeMakeNodeElementTable,
|
|
99
133
|
};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @license
|
|
5
3
|
* Copyright 2025 Google LLC
|
|
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/Interacti
|
|
|
10
8
|
|
|
11
9
|
import {Audit} from '../audit.js';
|
|
12
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
13
|
-
import {adaptInsightToAuditProduct,
|
|
11
|
+
import {adaptInsightToAuditProduct, maybeMakeNodeElementTable} from './insight-audit.js';
|
|
14
12
|
|
|
15
13
|
// eslint-disable-next-line max-len
|
|
16
14
|
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/InteractionToNextPaint.js', UIStrings);
|
|
@@ -36,15 +34,35 @@ class InteractionToNextPaintInsight extends Audit {
|
|
|
36
34
|
* @return {Promise<LH.Audit.Product>}
|
|
37
35
|
*/
|
|
38
36
|
static async audit(artifacts, context) {
|
|
39
|
-
// TODO: implement.
|
|
40
37
|
return adaptInsightToAuditProduct(artifacts, context, 'InteractionToNextPaint', (insight) => {
|
|
38
|
+
const event = insight.longestInteractionEvent;
|
|
39
|
+
if (!event) {
|
|
40
|
+
// TODO: show UIStrings.noInteractions?
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
41
44
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
42
45
|
const headings = [
|
|
46
|
+
{key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
|
|
47
|
+
{key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
|
|
43
48
|
];
|
|
49
|
+
|
|
44
50
|
/** @type {LH.Audit.Details.Table['items']} */
|
|
45
51
|
const items = [
|
|
52
|
+
/* eslint-disable max-len */
|
|
53
|
+
{phase: 'inputDelay', label: str_(UIStrings.inputDelay), duration: event.inputDelay / 1000},
|
|
54
|
+
{phase: 'processingDuration', label: str_(UIStrings.processingDuration), duration: event.mainThreadHandling / 1000},
|
|
55
|
+
{phase: 'presentationDelay', label: str_(UIStrings.presentationDelay), duration: event.presentationDelay / 1000},
|
|
56
|
+
/* eslint-enable max-len */
|
|
46
57
|
];
|
|
47
|
-
|
|
58
|
+
|
|
59
|
+
return Audit.makeListDetails([
|
|
60
|
+
maybeMakeNodeElementTable(
|
|
61
|
+
artifacts.TraceElements,
|
|
62
|
+
event.args.data.beginEvent.args.data.nodeId,
|
|
63
|
+
str_(i18n.UIStrings.columnElement)),
|
|
64
|
+
Audit.makeTableDetails(headings, items),
|
|
65
|
+
].filter(table => !!table));
|
|
48
66
|
});
|
|
49
67
|
}
|
|
50
68
|
}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export default LCPPhasesInsight;
|
|
2
2
|
declare class LCPPhasesInsight extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPPhases.js').LCPPhasesInsightModel>['phases']} phases
|
|
5
|
+
* @return {LH.Audit.Details.Table}
|
|
6
|
+
*/
|
|
7
|
+
static makePhaseTable(phases: Required<import("@paulirish/trace_engine/models/trace/insights/LCPPhases.js").LCPPhasesInsightModel>["phases"]): LH.Audit.Details.Table;
|
|
3
8
|
/**
|
|
4
9
|
* @param {LH.Artifacts} artifacts
|
|
5
10
|
* @param {LH.Audit.Context} context
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @license
|
|
5
3
|
* Copyright 2025 Google LLC
|
|
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/LCPPhases
|
|
|
10
8
|
|
|
11
9
|
import {Audit} from '../audit.js';
|
|
12
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
13
|
-
import {adaptInsightToAuditProduct,
|
|
11
|
+
import {adaptInsightToAuditProduct, maybeMakeNodeElementTable} from './insight-audit.js';
|
|
14
12
|
|
|
15
13
|
// eslint-disable-next-line max-len
|
|
16
14
|
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/LCPPhases.js', UIStrings);
|
|
@@ -30,21 +28,57 @@ class LCPPhasesInsight extends Audit {
|
|
|
30
28
|
};
|
|
31
29
|
}
|
|
32
30
|
|
|
31
|
+
/**
|
|
32
|
+
* @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPPhases.js').LCPPhasesInsightModel>['phases']} phases
|
|
33
|
+
* @return {LH.Audit.Details.Table}
|
|
34
|
+
*/
|
|
35
|
+
static makePhaseTable(phases) {
|
|
36
|
+
const {ttfb, loadDelay, loadTime, renderDelay} = phases;
|
|
37
|
+
|
|
38
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
39
|
+
const headings = [
|
|
40
|
+
{key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
|
|
41
|
+
{key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** @type {LH.Audit.Details.Table['items']} */
|
|
45
|
+
let items = [
|
|
46
|
+
/* eslint-disable max-len */
|
|
47
|
+
{phase: 'timeToFirstByte', label: str_(UIStrings.timeToFirstByte), duration: ttfb},
|
|
48
|
+
{phase: 'resourceLoadDelay', label: str_(UIStrings.resourceLoadDelay), duration: loadDelay},
|
|
49
|
+
{phase: 'resourceLoadDuration', label: str_(UIStrings.resourceLoadDuration), duration: loadTime},
|
|
50
|
+
{phase: 'elementRenderDelay', label: str_(UIStrings.elementRenderDelay), duration: renderDelay},
|
|
51
|
+
/* eslint-enable max-len */
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (loadDelay === undefined) {
|
|
55
|
+
items = items.filter(item => item.phase !== 'resourceLoadDelay');
|
|
56
|
+
}
|
|
57
|
+
if (loadTime === undefined) {
|
|
58
|
+
items = items.filter(item => item.phase !== 'resourceLoadDuration');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Audit.makeTableDetails(headings, items);
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* @param {LH.Artifacts} artifacts
|
|
35
66
|
* @param {LH.Audit.Context} context
|
|
36
67
|
* @return {Promise<LH.Audit.Product>}
|
|
37
68
|
*/
|
|
38
69
|
static async audit(artifacts, context) {
|
|
39
|
-
// TODO: implement.
|
|
40
70
|
return adaptInsightToAuditProduct(artifacts, context, 'LCPPhases', (insight) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
if (!insight.phases) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return Audit.makeListDetails([
|
|
76
|
+
maybeMakeNodeElementTable(
|
|
77
|
+
artifacts.TraceElements,
|
|
78
|
+
insight.lcpEvent?.args.data?.nodeId,
|
|
79
|
+
str_(i18n.UIStrings.columnElement)),
|
|
80
|
+
LCPPhasesInsight.makePhaseTable(insight.phases),
|
|
81
|
+
].filter(table => table !== undefined));
|
|
48
82
|
});
|
|
49
83
|
}
|
|
50
84
|
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
export default ThirdPartiesInsight;
|
|
2
|
+
export type URLSummary = {
|
|
3
|
+
transferSize: number;
|
|
4
|
+
mainThreadTime: number;
|
|
5
|
+
url: string | LH.IcuMessage;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* @typedef URLSummary
|
|
9
|
+
* @property {number} transferSize
|
|
10
|
+
* @property {number} mainThreadTime
|
|
11
|
+
* @property {string | LH.IcuMessage} url
|
|
12
|
+
*/
|
|
2
13
|
declare class ThirdPartiesInsight extends Audit {
|
|
14
|
+
/**
|
|
15
|
+
* @param {LH.Artifacts.Entity} entity
|
|
16
|
+
* @param {import('@paulirish/trace_engine/models/trace/insights/ThirdParties.js').ThirdPartiesInsightModel} insight
|
|
17
|
+
* @return {Array<URLSummary>}
|
|
18
|
+
*/
|
|
19
|
+
static makeSubItems(entity: LH.Artifacts.Entity, insight: import("@paulirish/trace_engine/models/trace/insights/ThirdParties.js").ThirdPartiesInsightModel): Array<URLSummary>;
|
|
3
20
|
/**
|
|
4
21
|
* @param {LH.Artifacts} artifacts
|
|
5
22
|
* @param {LH.Audit.Context} context
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* @license
|
|
5
3
|
* Copyright 2025 Google LLC
|
|
@@ -10,11 +8,18 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/ThirdPart
|
|
|
10
8
|
|
|
11
9
|
import {Audit} from '../audit.js';
|
|
12
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
13
|
-
import {adaptInsightToAuditProduct
|
|
11
|
+
import {adaptInsightToAuditProduct} from './insight-audit.js';
|
|
14
12
|
|
|
15
13
|
// eslint-disable-next-line max-len
|
|
16
14
|
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/ThirdParties.js', UIStrings);
|
|
17
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @typedef URLSummary
|
|
18
|
+
* @property {number} transferSize
|
|
19
|
+
* @property {number} mainThreadTime
|
|
20
|
+
* @property {string | LH.IcuMessage} url
|
|
21
|
+
*/
|
|
22
|
+
|
|
18
23
|
class ThirdPartiesInsight extends Audit {
|
|
19
24
|
/**
|
|
20
25
|
* @return {LH.Audit.Meta}
|
|
@@ -30,21 +35,53 @@ class ThirdPartiesInsight extends Audit {
|
|
|
30
35
|
};
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @param {LH.Artifacts.Entity} entity
|
|
40
|
+
* @param {import('@paulirish/trace_engine/models/trace/insights/ThirdParties.js').ThirdPartiesInsightModel} insight
|
|
41
|
+
* @return {Array<URLSummary>}
|
|
42
|
+
*/
|
|
43
|
+
static makeSubItems(entity, insight) {
|
|
44
|
+
const urls = [...insight.urlsByEntity.get(entity) ?? []];
|
|
45
|
+
return urls
|
|
46
|
+
.map(url => ({
|
|
47
|
+
url,
|
|
48
|
+
mainThreadTime: 0,
|
|
49
|
+
transferSize: 0,
|
|
50
|
+
...insight.summaryByUrl.get(url),
|
|
51
|
+
}))
|
|
52
|
+
// Sort by main thread time first, then transfer size to break ties.
|
|
53
|
+
.sort((a, b) => (b.mainThreadTime - a.mainThreadTime) || (b.transferSize - a.transferSize));
|
|
54
|
+
}
|
|
55
|
+
|
|
33
56
|
/**
|
|
34
57
|
* @param {LH.Artifacts} artifacts
|
|
35
58
|
* @param {LH.Audit.Context} context
|
|
36
59
|
* @return {Promise<LH.Audit.Product>}
|
|
37
60
|
*/
|
|
38
61
|
static async audit(artifacts, context) {
|
|
39
|
-
// TODO: implement.
|
|
40
62
|
return adaptInsightToAuditProduct(artifacts, context, 'ThirdParties', (insight) => {
|
|
63
|
+
const thirdPartyEntities = [...insight.summaryByEntity.entries()]
|
|
64
|
+
.filter((([entity, _]) => entity !== insight.firstPartyEntity));
|
|
65
|
+
|
|
41
66
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
42
67
|
const headings = [
|
|
68
|
+
/* eslint-disable max-len */
|
|
69
|
+
{key: 'entity', valueType: 'text', label: str_(UIStrings.columnThirdParty), subItemsHeading: {key: 'url', valueType: 'url'}},
|
|
70
|
+
{key: 'transferSize', granularity: 1, valueType: 'bytes', label: str_(UIStrings.columnTransferSize), subItemsHeading: {key: 'transferSize'}},
|
|
71
|
+
{key: 'mainThreadTime', granularity: 1, valueType: 'ms', label: str_(UIStrings.columnMainThreadTime), subItemsHeading: {key: 'mainThreadTime'}},
|
|
72
|
+
/* eslint-enable max-len */
|
|
43
73
|
];
|
|
44
74
|
/** @type {LH.Audit.Details.Table['items']} */
|
|
45
|
-
const items = [
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
const items = thirdPartyEntities.map(([entity, summary]) => ({
|
|
76
|
+
entity: entity.name,
|
|
77
|
+
transferSize: summary.transferSize,
|
|
78
|
+
mainThreadTime: summary.mainThreadTime,
|
|
79
|
+
subItems: {
|
|
80
|
+
type: /** @type {const} */ ('subitems'),
|
|
81
|
+
items: ThirdPartiesInsight.makeSubItems(entity, insight),
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
return Audit.makeTableDetails(headings, items, {isEntityGrouped: true});
|
|
48
85
|
});
|
|
49
86
|
}
|
|
50
87
|
}
|
|
@@ -16,7 +16,6 @@ import * as i18n from '../lib/i18n/i18n.js';
|
|
|
16
16
|
import {MainThreadTasks} from '../computed/main-thread-tasks.js';
|
|
17
17
|
import {TotalBlockingTime} from '../computed/metrics/total-blocking-time.js';
|
|
18
18
|
import {Sentry} from '../lib/sentry.js';
|
|
19
|
-
import {Util} from '../../shared/util.js';
|
|
20
19
|
|
|
21
20
|
const UIStrings = {
|
|
22
21
|
/** Title of a diagnostic audit that provides detail on the main thread work the browser did to load the page. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
|
|
@@ -142,7 +141,6 @@ class MainThreadWorkBreakdown extends Audit {
|
|
|
142
141
|
|
|
143
142
|
return {
|
|
144
143
|
score,
|
|
145
|
-
scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
146
144
|
numericValue: totalExecutionTime,
|
|
147
145
|
numericUnit: 'millisecond',
|
|
148
146
|
displayValue: str_(i18n.UIStrings.seconds, {timeInMs: totalExecutionTime}),
|
|
@@ -13,6 +13,10 @@ declare class TraceEngineResult {
|
|
|
13
13
|
* @return {Promise<LH.Artifacts.TraceEngineResult>}
|
|
14
14
|
*/
|
|
15
15
|
static runTraceEngine(traceEvents: LH.TraceEvent[]): Promise<LH.Artifacts.TraceEngineResult>;
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('@paulirish/trace_engine/models/trace/insights/types.js').TraceInsightSets} insightSets
|
|
18
|
+
*/
|
|
19
|
+
static localizeInsights(insightSets: import("@paulirish/trace_engine/models/trace/insights/types.js").TraceInsightSets): void;
|
|
16
20
|
/**
|
|
17
21
|
* @param {{trace: LH.Trace}} data
|
|
18
22
|
* @param {LH.Artifacts.ComputedContext} context
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
7
8
|
import * as TraceEngine from '../lib/trace-engine.js';
|
|
8
9
|
import {makeComputedArtifact} from './computed-artifact.js';
|
|
9
10
|
import {CumulativeLayoutShift} from './metrics/cumulative-layout-shift.js';
|
|
@@ -27,9 +28,96 @@ class TraceEngineResult {
|
|
|
27
28
|
), {});
|
|
28
29
|
if (!processor.parsedTrace) throw new Error('No data');
|
|
29
30
|
if (!processor.insights) throw new Error('No insights');
|
|
31
|
+
this.localizeInsights(processor.insights);
|
|
30
32
|
return {data: processor.parsedTrace, insights: processor.insights};
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* @param {import('@paulirish/trace_engine/models/trace/insights/types.js').TraceInsightSets} insightSets
|
|
37
|
+
*/
|
|
38
|
+
static localizeInsights(insightSets) {
|
|
39
|
+
/**
|
|
40
|
+
* Execute `cb(traceEngineI18nObject)` on every i18n object, recursively. The cb return
|
|
41
|
+
* value replaces traceEngineI18nObject.
|
|
42
|
+
* @param {any} obj
|
|
43
|
+
* @param {(traceEngineI18nObject: {i18nId: string, values?: {}}) => LH.IcuMessage} cb
|
|
44
|
+
* @param {Set<object>} seen
|
|
45
|
+
*/
|
|
46
|
+
function recursiveReplaceLocalizableStrings(obj, cb, seen) {
|
|
47
|
+
if (seen.has(seen)) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
seen.add(obj);
|
|
52
|
+
|
|
53
|
+
if (obj instanceof Map) {
|
|
54
|
+
for (const [key, value] of obj) {
|
|
55
|
+
if (value && typeof value === 'object' && 'i18nId' in value) {
|
|
56
|
+
obj.set(key, cb(value));
|
|
57
|
+
} else {
|
|
58
|
+
recursiveReplaceLocalizableStrings(value, cb, seen);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
62
|
+
Object.keys(obj).forEach(key => {
|
|
63
|
+
const value = obj[key];
|
|
64
|
+
if (value && typeof value === 'object' && 'i18nId' in value) {
|
|
65
|
+
obj[key] = cb(value);
|
|
66
|
+
} else {
|
|
67
|
+
recursiveReplaceLocalizableStrings(value, cb, seen);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
} else if (Array.isArray(obj)) {
|
|
71
|
+
for (let i = 0; i < obj.length; i++) {
|
|
72
|
+
const value = obj[i];
|
|
73
|
+
if (value && typeof value === 'object' && 'i18nId' in value) {
|
|
74
|
+
obj[i] = cb(value);
|
|
75
|
+
} else {
|
|
76
|
+
recursiveReplaceLocalizableStrings(value, cb, seen);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const insightSet of insightSets.values()) {
|
|
83
|
+
for (const [name, model] of Object.entries(insightSet.model)) {
|
|
84
|
+
if (model instanceof Error) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type {Record<string, string>} */
|
|
89
|
+
let traceEngineUIStrings;
|
|
90
|
+
if (name in TraceEngine.Insights.Models) {
|
|
91
|
+
const nameAsKey = /** @type {keyof typeof TraceEngine.Insights.Models} */ (name);
|
|
92
|
+
traceEngineUIStrings = TraceEngine.Insights.Models[nameAsKey].UIStrings;
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error(`insight missing UIStrings: ${name}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const key = `node_modules/@paulirish/trace_engine/models/trace/insights/${name}.js`;
|
|
98
|
+
const str_ = i18n.createIcuMessageFn(key, traceEngineUIStrings);
|
|
99
|
+
|
|
100
|
+
// Pass `{i18nId: string, values?: {}}` through Lighthouse's i18n pipeline.
|
|
101
|
+
// This is equivalent to if we directly did `str_(UIStrings.whatever, ...)`
|
|
102
|
+
recursiveReplaceLocalizableStrings(model, (traceEngineI18nObject) => {
|
|
103
|
+
let values = traceEngineI18nObject.values;
|
|
104
|
+
if (values) {
|
|
105
|
+
values = structuredClone(values);
|
|
106
|
+
for (const [key, value] of Object.entries(values)) {
|
|
107
|
+
if (value && typeof value === 'object' && '__i18nBytes' in value) {
|
|
108
|
+
// @ts-expect-error
|
|
109
|
+
values[key] = value.__i18nBytes;
|
|
110
|
+
// TODO: use an actual byte formatter. Right now, this shows the exact number of bytes.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return str_(traceEngineI18nObject.i18nId, values);
|
|
116
|
+
}, new Set());
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
33
121
|
/**
|
|
34
122
|
* @param {{trace: LH.Trace}} data
|
|
35
123
|
* @param {LH.Artifacts.ComputedContext} context
|
|
@@ -408,20 +408,20 @@ const defaultConfig = {
|
|
|
408
408
|
{id: 'interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP'},
|
|
409
409
|
|
|
410
410
|
// Insight audits.
|
|
411
|
-
{id: 'cls-culprits-insight', weight: 0, group: '
|
|
412
|
-
{id: 'document-latency-insight', weight: 0, group: '
|
|
413
|
-
{id: 'dom-size-insight', weight: 0, group: '
|
|
414
|
-
{id: 'font-display-insight', weight: 0, group: '
|
|
415
|
-
{id: 'forced-reflow-insight', weight: 0, group: '
|
|
416
|
-
{id: 'image-delivery-insight', weight: 0, group: '
|
|
417
|
-
{id: 'interaction-to-next-paint-insight', weight: 0, group: '
|
|
418
|
-
{id: 'lcp-discovery-insight', weight: 0, group: '
|
|
419
|
-
{id: 'lcp-phases-insight', weight: 0, group: '
|
|
420
|
-
{id: 'long-critical-network-tree-insight', weight: 0, group: '
|
|
421
|
-
{id: 'render-blocking-insight', weight: 0, group: '
|
|
422
|
-
{id: 'slow-css-selector-insight', weight: 0, group: '
|
|
423
|
-
{id: 'third-parties-insight', weight: 0, group: '
|
|
424
|
-
{id: 'viewport-insight', weight: 0, group: '
|
|
411
|
+
{id: 'cls-culprits-insight', weight: 0, group: 'insights'},
|
|
412
|
+
{id: 'document-latency-insight', weight: 0, group: 'insights'},
|
|
413
|
+
{id: 'dom-size-insight', weight: 0, group: 'insights'},
|
|
414
|
+
{id: 'font-display-insight', weight: 0, group: 'insights'},
|
|
415
|
+
{id: 'forced-reflow-insight', weight: 0, group: 'insights'},
|
|
416
|
+
{id: 'image-delivery-insight', weight: 0, group: 'insights'},
|
|
417
|
+
{id: 'interaction-to-next-paint-insight', weight: 0, group: 'insights'},
|
|
418
|
+
{id: 'lcp-discovery-insight', weight: 0, group: 'insights'},
|
|
419
|
+
{id: 'lcp-phases-insight', weight: 0, group: 'insights'},
|
|
420
|
+
{id: 'long-critical-network-tree-insight', weight: 0, group: 'insights'},
|
|
421
|
+
{id: 'render-blocking-insight', weight: 0, group: 'insights'},
|
|
422
|
+
{id: 'slow-css-selector-insight', weight: 0, group: 'insights'},
|
|
423
|
+
{id: 'third-parties-insight', weight: 0, group: 'insights'},
|
|
424
|
+
{id: 'viewport-insight', weight: 0, group: 'insights'},
|
|
425
425
|
|
|
426
426
|
// These are our "invisible" metrics. Not displayed, but still in the LHR.
|
|
427
427
|
{id: 'interactive', weight: 0, group: 'hidden', acronym: 'TTI'},
|
|
@@ -92,7 +92,7 @@ class TraceElements extends BaseGatherer {
|
|
|
92
92
|
/**
|
|
93
93
|
* Execute `cb(obj, key)` on every object property (non-objects only), recursively.
|
|
94
94
|
* @param {any} obj
|
|
95
|
-
* @param {(obj: Record<string,
|
|
95
|
+
* @param {(obj: Record<string, unknown>, key: string) => void} cb
|
|
96
96
|
* @param {Set<object>} seen
|
|
97
97
|
*/
|
|
98
98
|
function recursiveObjectEnumerate(obj, cb, seen) {
|
|
@@ -7,5 +7,6 @@ export type SaneSyntheticLayoutShift = SyntheticLayoutShift & {
|
|
|
7
7
|
export const TraceProcessor: typeof TraceEngine.Processor.TraceProcessor;
|
|
8
8
|
export const TraceHandlers: typeof TraceEngine.Handlers.ModelHandlers;
|
|
9
9
|
export const RootCauses: typeof TraceEngine.RootCauses.RootCauses.RootCauses;
|
|
10
|
+
export const Insights: typeof TraceEngine.Insights;
|
|
10
11
|
import * as TraceEngine from '@paulirish/trace_engine';
|
|
11
12
|
//# sourceMappingURL=trace-engine.d.ts.map
|
package/core/lib/trace-engine.js
CHANGED
|
@@ -10,9 +10,11 @@ polyfillDOMRect();
|
|
|
10
10
|
const TraceProcessor = TraceEngine.Processor.TraceProcessor;
|
|
11
11
|
const TraceHandlers = TraceEngine.Handlers.ModelHandlers;
|
|
12
12
|
const RootCauses = TraceEngine.RootCauses.RootCauses.RootCauses;
|
|
13
|
+
const Insights = TraceEngine.Insights;
|
|
13
14
|
|
|
14
15
|
export {
|
|
15
16
|
TraceProcessor,
|
|
16
17
|
TraceHandlers,
|
|
17
18
|
RootCauses,
|
|
19
|
+
Insights,
|
|
18
20
|
};
|