lighthouse 12.3.0-dev.20250209 → 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.
Files changed (36) hide show
  1. package/core/audits/bootup-time.js +0 -2
  2. package/core/audits/byte-efficiency/total-byte-weight.js +0 -2
  3. package/core/audits/dobetterweb/dom-size.js +0 -2
  4. package/core/audits/insights/image-delivery-insight.js +31 -6
  5. package/core/audits/insights/insight-audit.d.ts +7 -0
  6. package/core/audits/insights/insight-audit.js +35 -1
  7. package/core/audits/insights/interaction-to-next-paint-insight.js +23 -5
  8. package/core/audits/insights/lcp-phases-insight.d.ts +5 -0
  9. package/core/audits/insights/lcp-phases-insight.js +45 -11
  10. package/core/audits/insights/third-parties-insight.d.ts +17 -0
  11. package/core/audits/insights/third-parties-insight.js +44 -7
  12. package/core/audits/mainthread-work-breakdown.js +0 -2
  13. package/core/audits/seo/is-crawlable.d.ts +1 -0
  14. package/core/audits/server-response-time.js +0 -1
  15. package/core/computed/trace-engine-result.d.ts +4 -0
  16. package/core/computed/trace-engine-result.js +88 -0
  17. package/core/config/default-config.js +14 -14
  18. package/core/gather/gatherers/trace-elements.js +1 -1
  19. package/core/lib/trace-engine.d.ts +1 -0
  20. package/core/lib/trace-engine.js +2 -0
  21. package/dist/report/bundle.esm.js +13 -10
  22. package/dist/report/flow.js +8 -5
  23. package/dist/report/standalone.js +7 -4
  24. package/flow-report/src/i18n/i18n.d.ts +2 -0
  25. package/package.json +2 -2
  26. package/report/assets/styles.css +3 -0
  27. package/report/assets/templates.html +1 -0
  28. package/report/renderer/components.js +8 -2
  29. package/report/renderer/performance-category-renderer.d.ts +10 -0
  30. package/report/renderer/performance-category-renderer.js +34 -23
  31. package/report/renderer/report-utils.d.ts +1 -0
  32. package/report/renderer/report-utils.js +2 -0
  33. package/report/renderer/topbar-features.js +8 -0
  34. package/shared/localization/locales/en-US.json +8 -2
  35. package/shared/localization/locales/en-XL.json +8 -2
  36. 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, makeNodeItemForNodeId} from './insight-audit.js';
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: insight.shouldShow ? 0 : 1,
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, makeNodeItemForNodeId} from './insight-audit.js';
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
- return Audit.makeTableDetails(headings, items);
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, makeNodeItemForNodeId} from './insight-audit.js';
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
- /** @type {LH.Audit.Details.Table['headings']} */
42
- const headings = [
43
- ];
44
- /** @type {LH.Audit.Details.Table['items']} */
45
- const items = [
46
- ];
47
- return Audit.makeTableDetails(headings, items);
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, makeNodeItemForNodeId} from './insight-audit.js';
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
- return Audit.makeTableDetails(headings, items);
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}),
@@ -12,6 +12,7 @@ declare class IsCrawlable extends Audit {
12
12
  selector?: string;
13
13
  boundingRect?: import("../../../types/lhr/audit-details.js").default.Rect;
14
14
  nodeLabel?: string;
15
+ explanation?: string;
15
16
  };
16
17
  } | undefined;
17
18
  /**
@@ -92,7 +92,6 @@ class ServerResponseTime extends Audit {
92
92
  numericValue: responseTime,
93
93
  numericUnit: 'millisecond',
94
94
  score: Number(passed),
95
- scoreDisplayMode: passed ? Audit.SCORING_MODES.INFORMATIVE : undefined,
96
95
  displayValue,
97
96
  details,
98
97
  metricSavings: {
@@ -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: 'hidden'},
412
- {id: 'document-latency-insight', weight: 0, group: 'hidden'},
413
- {id: 'dom-size-insight', weight: 0, group: 'hidden'},
414
- {id: 'font-display-insight', weight: 0, group: 'hidden'},
415
- {id: 'forced-reflow-insight', weight: 0, group: 'hidden'},
416
- {id: 'image-delivery-insight', weight: 0, group: 'hidden'},
417
- {id: 'interaction-to-next-paint-insight', weight: 0, group: 'hidden'},
418
- {id: 'lcp-discovery-insight', weight: 0, group: 'hidden'},
419
- {id: 'lcp-phases-insight', weight: 0, group: 'hidden'},
420
- {id: 'long-critical-network-tree-insight', weight: 0, group: 'hidden'},
421
- {id: 'render-blocking-insight', weight: 0, group: 'hidden'},
422
- {id: 'slow-css-selector-insight', weight: 0, group: 'hidden'},
423
- {id: 'third-parties-insight', weight: 0, group: 'hidden'},
424
- {id: 'viewport-insight', weight: 0, group: 'hidden'},
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, string>, key: string) => void} cb
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
@@ -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
  };