lighthouse 12.3.0 → 12.4.0

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 (142) hide show
  1. package/cli/test/smokehouse/core-tests.js +4 -0
  2. package/core/audits/audit.d.ts +5 -0
  3. package/core/audits/audit.js +12 -0
  4. package/core/audits/bootup-time.js +0 -2
  5. package/core/audits/byte-efficiency/duplicated-javascript.d.ts +4 -5
  6. package/core/audits/byte-efficiency/duplicated-javascript.js +9 -5
  7. package/core/audits/byte-efficiency/legacy-javascript.d.ts +2 -2
  8. package/core/audits/byte-efficiency/legacy-javascript.js +17 -5
  9. package/core/audits/byte-efficiency/polyfill-graph-data.json +48 -49
  10. package/core/audits/byte-efficiency/total-byte-weight.js +0 -2
  11. package/core/audits/clickjacking-mitigation.d.ts +42 -0
  12. package/core/audits/clickjacking-mitigation.js +139 -0
  13. package/core/audits/dobetterweb/dom-size.js +0 -2
  14. package/core/audits/insights/README.md +3 -0
  15. package/core/audits/insights/cls-culprits-insight.d.ts +25 -0
  16. package/core/audits/insights/cls-culprits-insight.js +137 -0
  17. package/core/audits/insights/document-latency-insight.d.ts +11 -0
  18. package/core/audits/insights/document-latency-insight.js +48 -0
  19. package/core/audits/insights/dom-size-insight.d.ts +11 -0
  20. package/core/audits/insights/dom-size-insight.js +85 -0
  21. package/core/audits/insights/font-display-insight.d.ts +11 -0
  22. package/core/audits/insights/font-display-insight.js +53 -0
  23. package/core/audits/insights/forced-reflow-insight.d.ts +11 -0
  24. package/core/audits/insights/forced-reflow-insight.js +52 -0
  25. package/core/audits/insights/image-delivery-insight.d.ts +11 -0
  26. package/core/audits/insights/image-delivery-insight.js +83 -0
  27. package/core/audits/insights/insight-audit.d.ts +23 -0
  28. package/core/audits/insights/insight-audit.js +133 -0
  29. package/core/audits/insights/interaction-to-next-paint-insight.d.ts +11 -0
  30. package/core/audits/insights/interaction-to-next-paint-insight.js +71 -0
  31. package/core/audits/insights/lcp-discovery-insight.d.ts +11 -0
  32. package/core/audits/insights/lcp-discovery-insight.js +48 -0
  33. package/core/audits/insights/lcp-phases-insight.d.ts +16 -0
  34. package/core/audits/insights/lcp-phases-insight.js +87 -0
  35. package/core/audits/insights/long-critical-network-tree-insight.d.ts +11 -0
  36. package/core/audits/insights/long-critical-network-tree-insight.js +53 -0
  37. package/core/audits/insights/render-blocking-insight.d.ts +11 -0
  38. package/core/audits/insights/render-blocking-insight.js +57 -0
  39. package/core/audits/insights/slow-css-selector-insight.d.ts +11 -0
  40. package/core/audits/insights/slow-css-selector-insight.js +52 -0
  41. package/core/audits/insights/third-parties-insight.d.ts +28 -0
  42. package/core/audits/insights/third-parties-insight.js +90 -0
  43. package/core/audits/insights/viewport-insight.d.ts +11 -0
  44. package/core/audits/insights/viewport-insight.js +54 -0
  45. package/core/audits/layout-shifts.d.ts +0 -1
  46. package/core/audits/layout-shifts.js +18 -21
  47. package/core/audits/mainthread-work-breakdown.js +0 -2
  48. package/core/audits/seo/is-crawlable.d.ts +1 -0
  49. package/core/audits/server-response-time.js +0 -1
  50. package/core/computed/metrics/lantern-metric.js +5 -1
  51. package/core/computed/trace-engine-result.js +71 -17
  52. package/core/config/default-config.js +37 -1
  53. package/core/gather/gatherers/inspector-issues.js +3 -0
  54. package/core/gather/gatherers/trace-elements.d.ts +10 -2
  55. package/core/gather/gatherers/trace-elements.js +89 -12
  56. package/core/lib/bf-cache-strings.d.ts +7 -4
  57. package/core/lib/bf-cache-strings.js +174 -140
  58. package/core/lib/cdt/generated/ParsedURL.d.ts +1 -0
  59. package/core/lib/cdt/generated/ParsedURL.js +16 -4
  60. package/core/lib/cdt/generated/SourceMap.d.ts +32 -5
  61. package/core/lib/cdt/generated/SourceMap.js +192 -100
  62. package/core/lib/deprecations-strings.d.ts +78 -98
  63. package/core/lib/deprecations-strings.js +23 -41
  64. package/core/lib/i18n/i18n.d.ts +1 -0
  65. package/core/lib/i18n/i18n.js +2 -0
  66. package/core/lib/trace-engine.d.ts +1 -0
  67. package/core/lib/trace-engine.js +2 -0
  68. package/core/runner.js +2 -0
  69. package/dist/report/bundle.esm.js +196 -9
  70. package/dist/report/flow.js +197 -10
  71. package/dist/report/standalone.js +197 -10
  72. package/flow-report/src/i18n/i18n.d.ts +2 -0
  73. package/package.json +15 -13
  74. package/readme.md +3 -0
  75. package/report/assets/styles.css +179 -5
  76. package/report/assets/templates.html +14 -0
  77. package/report/renderer/components.js +9 -3
  78. package/report/renderer/details-renderer.d.ts +5 -0
  79. package/report/renderer/details-renderer.js +24 -0
  80. package/report/renderer/dom.d.ts +12 -1
  81. package/report/renderer/dom.js +26 -1
  82. package/report/renderer/i18n-formatter.d.ts +1 -1
  83. package/report/renderer/performance-category-renderer.d.ts +10 -0
  84. package/report/renderer/performance-category-renderer.js +81 -20
  85. package/report/renderer/report-utils.d.ts +1 -0
  86. package/report/renderer/report-utils.js +2 -0
  87. package/report/renderer/topbar-features.js +7 -0
  88. package/shared/localization/locales/ar-XB.json +74 -26
  89. package/shared/localization/locales/ar.json +76 -28
  90. package/shared/localization/locales/bg.json +74 -26
  91. package/shared/localization/locales/ca.json +74 -26
  92. package/shared/localization/locales/cs.json +74 -26
  93. package/shared/localization/locales/da.json +74 -26
  94. package/shared/localization/locales/de.json +75 -27
  95. package/shared/localization/locales/el.json +74 -26
  96. package/shared/localization/locales/en-GB.json +74 -26
  97. package/shared/localization/locales/en-US.json +288 -30
  98. package/shared/localization/locales/en-XA.json +48 -24
  99. package/shared/localization/locales/en-XL.json +288 -30
  100. package/shared/localization/locales/es-419.json +74 -26
  101. package/shared/localization/locales/es.json +74 -26
  102. package/shared/localization/locales/fi.json +74 -26
  103. package/shared/localization/locales/fil.json +75 -27
  104. package/shared/localization/locales/fr.json +74 -26
  105. package/shared/localization/locales/he.json +82 -34
  106. package/shared/localization/locales/hi.json +74 -26
  107. package/shared/localization/locales/hr.json +75 -27
  108. package/shared/localization/locales/hu.json +74 -26
  109. package/shared/localization/locales/id.json +74 -26
  110. package/shared/localization/locales/it.json +85 -37
  111. package/shared/localization/locales/ja.json +75 -27
  112. package/shared/localization/locales/ko.json +75 -27
  113. package/shared/localization/locales/lt.json +75 -27
  114. package/shared/localization/locales/lv.json +74 -26
  115. package/shared/localization/locales/nl.json +74 -26
  116. package/shared/localization/locales/no.json +75 -27
  117. package/shared/localization/locales/pl.json +74 -26
  118. package/shared/localization/locales/pt-PT.json +74 -26
  119. package/shared/localization/locales/pt.json +74 -26
  120. package/shared/localization/locales/ro.json +74 -26
  121. package/shared/localization/locales/ru.json +74 -26
  122. package/shared/localization/locales/sk.json +74 -26
  123. package/shared/localization/locales/sl.json +74 -26
  124. package/shared/localization/locales/sr-Latn.json +74 -26
  125. package/shared/localization/locales/sr.json +74 -26
  126. package/shared/localization/locales/sv.json +74 -26
  127. package/shared/localization/locales/ta.json +74 -26
  128. package/shared/localization/locales/te.json +74 -26
  129. package/shared/localization/locales/th.json +76 -28
  130. package/shared/localization/locales/tr.json +74 -26
  131. package/shared/localization/locales/uk.json +74 -26
  132. package/shared/localization/locales/vi.json +74 -26
  133. package/shared/localization/locales/zh-HK.json +75 -27
  134. package/shared/localization/locales/zh-TW.json +74 -26
  135. package/shared/localization/locales/zh.json +74 -26
  136. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +3 -0
  137. package/types/artifacts.d.ts +5 -3
  138. package/types/audit.d.ts +2 -0
  139. package/types/lhr/audit-details.d.ts +13 -1
  140. package/types/lhr/audit-result.d.ts +2 -0
  141. package/core/gather/gatherers/root-causes.d.ts +0 -19
  142. package/core/gather/gatherers/root-causes.js +0 -144
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {UIStrings as InsightUIStrings} from '@paulirish/trace_engine/models/trace/insights/CLSCulprits.js';
8
+
9
+ import {Audit} from '../audit.js';
10
+ import * as i18n from '../../lib/i18n/i18n.js';
11
+ import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
12
+ import TraceElements from '../../gather/gatherers/trace-elements.js';
13
+ import {CumulativeLayoutShift} from '../../computed/metrics/cumulative-layout-shift.js';
14
+
15
+ const MAX_LAYOUT_SHIFTS_PER_CLUSTER = 5;
16
+
17
+ /** @typedef {{extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue, cause: LH.IcuMessage}} SubItem */
18
+
19
+ // eslint-disable-next-line max-len
20
+ const insightStr_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/CLSCulprits.js', InsightUIStrings);
21
+
22
+ /* eslint-disable max-len */
23
+ const UIStrings = {
24
+ /** Label for a column in a data table; entries in this column will be a number representing how large the layout shift was. */
25
+ columnScore: 'Layout shift score',
26
+ };
27
+ /* eslint-enable max-len */
28
+
29
+ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
30
+
31
+ class CLSCulpritsInsight extends Audit {
32
+ /**
33
+ * @return {LH.Audit.Meta}
34
+ */
35
+ static get meta() {
36
+ return {
37
+ id: 'cls-culprits-insight',
38
+ title: insightStr_(InsightUIStrings.title),
39
+ failureTitle: insightStr_(InsightUIStrings.title),
40
+ description: insightStr_(InsightUIStrings.description),
41
+ guidanceLevel: 3,
42
+ requiredArtifacts: ['traces', 'TraceElements'],
43
+ replacesAudits: ['layout-shifts', 'non-composited-animations', 'unsized-images'],
44
+ };
45
+ }
46
+
47
+ /**
48
+ * @param {import('@paulirish/trace_engine/models/trace/insights/CLSCulprits.js').CLSCulpritsInsightModel} insight
49
+ * @param {import('../../lib/trace-engine.js').SaneSyntheticLayoutShift} event
50
+ * @param {LH.Artifacts.TraceElement[]} TraceElements
51
+ * @return {LH.Audit.Details.TableSubItems|undefined}
52
+ */
53
+ static getCulpritSubItems(insight, event, TraceElements) {
54
+ const culprits = insight.shifts.get(event);
55
+ if (!culprits) {
56
+ return;
57
+ }
58
+
59
+ /** @type {SubItem[]} */
60
+ const subItems = [];
61
+ for (const backendNodeId of culprits.unsizedImages) {
62
+ subItems.push({
63
+ extra: makeNodeItemForNodeId(TraceElements, backendNodeId),
64
+ cause: insightStr_(InsightUIStrings.unsizedImages),
65
+ });
66
+ }
67
+ for (const request of culprits.fontRequests) {
68
+ const url = request.args.data.url;
69
+ subItems.push({
70
+ extra: {type: 'url', value: url},
71
+ cause: insightStr_(InsightUIStrings.fontRequest),
72
+ });
73
+ }
74
+ if (culprits.iframeIds.length) {
75
+ subItems.push({
76
+ cause: insightStr_(InsightUIStrings.injectedIframe),
77
+ });
78
+ }
79
+
80
+ if (subItems.length) {
81
+ return {type: 'subitems', items: subItems};
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @param {LH.Artifacts} artifacts
87
+ * @param {LH.Audit.Context} context
88
+ * @return {Promise<LH.Audit.Product>}
89
+ */
90
+ static async audit(artifacts, context) {
91
+ return adaptInsightToAuditProduct(artifacts, context, 'CLSCulprits', (insight) => {
92
+ /** @type {LH.Audit.Details.Table['headings']} */
93
+ const headings = [
94
+ /* eslint-disable max-len */
95
+ {key: 'node', valueType: 'node', subItemsHeading: {key: 'extra'}, label: insightStr_(i18n.UIStrings.columnElement)},
96
+ {key: 'score', valueType: 'numeric', subItemsHeading: {key: 'cause', valueType: 'text'}, granularity: 0.001, label: str_(UIStrings.columnScore)},
97
+ /* eslint-enable max-len */
98
+ ];
99
+
100
+ const tables = insight.clusters.map(cluster => {
101
+ const events =
102
+ /** @type {import('../../lib/trace-engine.js').SaneSyntheticLayoutShift[]} */ (
103
+ cluster.events.filter(e => !!e.args.data)
104
+ ).sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta)
105
+ .slice(0, MAX_LAYOUT_SHIFTS_PER_CLUSTER);
106
+ const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(events.map(e => ({
107
+ impactedNodes: e.args.data.impacted_nodes,
108
+ ts: e.ts,
109
+ isMainFrame: e.args.data.is_main_frame,
110
+ weightedScore: e.args.data.weighted_score_delta,
111
+ event: /** @type {any} */ (e),
112
+ })));
113
+
114
+ /** @type {LH.Audit.Details.Table['items']} */
115
+ const items = events.map(event => {
116
+ const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
117
+ event.args.data.impacted_nodes || [], impactByNodeId, event);
118
+ return {
119
+ node: makeNodeItemForNodeId(artifacts.TraceElements, biggestImpactNodeId),
120
+ score: event.args.data?.weighted_score_delta,
121
+ subItems: this.getCulpritSubItems(insight, event, artifacts.TraceElements),
122
+ };
123
+ });
124
+ items.unshift({
125
+ node: {type: 'text', value: insightStr_(i18n.UIStrings.total)},
126
+ score: cluster.clusterCumulativeScore,
127
+ });
128
+ return Audit.makeTableDetails(headings, items);
129
+ });
130
+
131
+ return Audit.makeListDetails(tables);
132
+ });
133
+ }
134
+ }
135
+
136
+ export default CLSCulpritsInsight;
137
+ export {UIStrings};
@@ -0,0 +1,11 @@
1
+ export default DocumentLatencyInsight;
2
+ declare class DocumentLatencyInsight extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
7
+ */
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
+ }
10
+ import { Audit } from '../audit.js';
11
+ //# sourceMappingURL=document-latency-insight.d.ts.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/DocumentLatency.js';
8
+
9
+ import {Audit} from '../audit.js';
10
+ import * as i18n from '../../lib/i18n/i18n.js';
11
+ import {adaptInsightToAuditProduct} from './insight-audit.js';
12
+
13
+ // eslint-disable-next-line max-len
14
+ const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/DocumentLatency.js', UIStrings);
15
+
16
+ class DocumentLatencyInsight extends Audit {
17
+ /**
18
+ * @return {LH.Audit.Meta}
19
+ */
20
+ static get meta() {
21
+ return {
22
+ id: 'document-latency-insight',
23
+ title: str_(UIStrings.title),
24
+ failureTitle: str_(UIStrings.title),
25
+ description: str_(UIStrings.description),
26
+ guidanceLevel: 3,
27
+ requiredArtifacts: ['traces', 'TraceElements'],
28
+ replacesAudits: ['redirects', 'server-response-time', 'uses-text-compression'],
29
+ };
30
+ }
31
+
32
+ /**
33
+ * @param {LH.Artifacts} artifacts
34
+ * @param {LH.Audit.Context} context
35
+ * @return {Promise<LH.Audit.Product>}
36
+ */
37
+ static async audit(artifacts, context) {
38
+ return adaptInsightToAuditProduct(artifacts, context, 'DocumentLatency', (insight) => {
39
+ if (!insight.data) {
40
+ return;
41
+ }
42
+
43
+ return Audit.makeChecklistDetails(insight.data.checklist);
44
+ });
45
+ }
46
+ }
47
+
48
+ export default DocumentLatencyInsight;
@@ -0,0 +1,11 @@
1
+ export default DOMSizeInsight;
2
+ declare class DOMSizeInsight extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
7
+ */
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
+ }
10
+ import { Audit } from '../audit.js';
11
+ //# sourceMappingURL=dom-size-insight.d.ts.map
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/DOMSize.js';
8
+
9
+ import {Audit} from '../audit.js';
10
+ import * as i18n from '../../lib/i18n/i18n.js';
11
+ import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
12
+
13
+ // eslint-disable-next-line max-len
14
+ const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/DOMSize.js', UIStrings);
15
+
16
+ class DOMSizeInsight extends Audit {
17
+ /**
18
+ * @return {LH.Audit.Meta}
19
+ */
20
+ static get meta() {
21
+ return {
22
+ id: 'dom-size-insight',
23
+ title: str_(UIStrings.title),
24
+ failureTitle: str_(UIStrings.title),
25
+ description: str_(UIStrings.description),
26
+ guidanceLevel: 3,
27
+ requiredArtifacts: ['traces', 'TraceElements'],
28
+ replacesAudits: ['dom-size'],
29
+ };
30
+ }
31
+
32
+ /**
33
+ * @param {LH.Artifacts} artifacts
34
+ * @param {LH.Audit.Context} context
35
+ * @return {Promise<LH.Audit.Product>}
36
+ */
37
+ static async audit(artifacts, context) {
38
+ return adaptInsightToAuditProduct(artifacts, context, 'DOMSize', (insight) => {
39
+ if (!insight.maxDOMStats?.args.data.maxChildren || !insight.maxDOMStats?.args.data.maxDepth) {
40
+ return;
41
+ }
42
+
43
+ const {totalElements, maxChildren, maxDepth} = insight.maxDOMStats.args.data;
44
+
45
+ /** @type {LH.Audit.Details.Table['headings']} */
46
+ const headings = [
47
+ {key: 'statistic', valueType: 'text', label: str_(UIStrings.statistic)},
48
+ {key: 'node', valueType: 'node', label: str_(UIStrings.element)},
49
+ {key: 'value', valueType: 'numeric', label: str_(UIStrings.value)},
50
+ ];
51
+ /** @type {LH.Audit.Details.Table['items']} */
52
+ const items = [
53
+ {
54
+ statistic: str_(UIStrings.totalElements),
55
+ value: {
56
+ type: 'numeric',
57
+ granularity: 1,
58
+ value: totalElements,
59
+ },
60
+ },
61
+ {
62
+ statistic: str_(UIStrings.maxChildren),
63
+ node: makeNodeItemForNodeId(artifacts.TraceElements, maxChildren.nodeId),
64
+ value: {
65
+ type: 'numeric',
66
+ granularity: 1,
67
+ value: maxChildren.numChildren,
68
+ },
69
+ },
70
+ {
71
+ statistic: str_(UIStrings.maxDOMDepth),
72
+ node: makeNodeItemForNodeId(artifacts.TraceElements, maxDepth.nodeId),
73
+ value: {
74
+ type: 'numeric',
75
+ granularity: 1,
76
+ value: maxDepth.depth,
77
+ },
78
+ },
79
+ ];
80
+ return Audit.makeTableDetails(headings, items);
81
+ });
82
+ }
83
+ }
84
+
85
+ export default DOMSizeInsight;
@@ -0,0 +1,11 @@
1
+ export default FontDisplayInsight;
2
+ declare class FontDisplayInsight extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
7
+ */
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
+ }
10
+ import { Audit } from '../audit.js';
11
+ //# sourceMappingURL=font-display-insight.d.ts.map
@@ -0,0 +1,53 @@
1
+ /* eslint-disable no-unused-vars */ // TODO: remove once implemented.
2
+
3
+ /**
4
+ * @license
5
+ * Copyright 2025 Google LLC
6
+ * SPDX-License-Identifier: Apache-2.0
7
+ */
8
+
9
+ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/FontDisplay.js';
10
+
11
+ import {Audit} from '../audit.js';
12
+ import * as i18n from '../../lib/i18n/i18n.js';
13
+ import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
14
+
15
+ // eslint-disable-next-line max-len
16
+ const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/FontDisplay.js', UIStrings);
17
+
18
+ class FontDisplayInsight extends Audit {
19
+ /**
20
+ * @return {LH.Audit.Meta}
21
+ */
22
+ static get meta() {
23
+ return {
24
+ id: 'font-display-insight',
25
+ title: str_(UIStrings.title),
26
+ failureTitle: str_(UIStrings.title),
27
+ description: str_(UIStrings.description),
28
+ guidanceLevel: 3,
29
+ requiredArtifacts: ['traces', 'TraceElements'],
30
+ replacesAudits: ['font-display'],
31
+ };
32
+ }
33
+
34
+ /**
35
+ * @param {LH.Artifacts} artifacts
36
+ * @param {LH.Audit.Context} context
37
+ * @return {Promise<LH.Audit.Product>}
38
+ */
39
+ static async audit(artifacts, context) {
40
+ // TODO: implement.
41
+ return adaptInsightToAuditProduct(artifacts, context, 'FontDisplay', (insight) => {
42
+ /** @type {LH.Audit.Details.Table['headings']} */
43
+ const headings = [
44
+ ];
45
+ /** @type {LH.Audit.Details.Table['items']} */
46
+ const items = [
47
+ ];
48
+ return Audit.makeTableDetails(headings, items);
49
+ });
50
+ }
51
+ }
52
+
53
+ export default FontDisplayInsight;
@@ -0,0 +1,11 @@
1
+ export default ForcedReflowInsight;
2
+ declare class ForcedReflowInsight extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
7
+ */
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
+ }
10
+ import { Audit } from '../audit.js';
11
+ //# sourceMappingURL=forced-reflow-insight.d.ts.map
@@ -0,0 +1,52 @@
1
+ /* eslint-disable no-unused-vars */ // TODO: remove once implemented.
2
+
3
+ /**
4
+ * @license
5
+ * Copyright 2025 Google LLC
6
+ * SPDX-License-Identifier: Apache-2.0
7
+ */
8
+
9
+ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/ForcedReflow.js';
10
+
11
+ import {Audit} from '../audit.js';
12
+ import * as i18n from '../../lib/i18n/i18n.js';
13
+ import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
14
+
15
+ // eslint-disable-next-line max-len
16
+ const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/ForcedReflow.js', UIStrings);
17
+
18
+ class ForcedReflowInsight extends Audit {
19
+ /**
20
+ * @return {LH.Audit.Meta}
21
+ */
22
+ static get meta() {
23
+ return {
24
+ id: 'forced-reflow-insight',
25
+ title: str_(UIStrings.title),
26
+ failureTitle: str_(UIStrings.title),
27
+ description: str_(UIStrings.description),
28
+ guidanceLevel: 3,
29
+ requiredArtifacts: ['traces', 'TraceElements'],
30
+ };
31
+ }
32
+
33
+ /**
34
+ * @param {LH.Artifacts} artifacts
35
+ * @param {LH.Audit.Context} context
36
+ * @return {Promise<LH.Audit.Product>}
37
+ */
38
+ static async audit(artifacts, context) {
39
+ // TODO: implement.
40
+ return adaptInsightToAuditProduct(artifacts, context, 'ForcedReflow', (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);
48
+ });
49
+ }
50
+ }
51
+
52
+ export default ForcedReflowInsight;
@@ -0,0 +1,11 @@
1
+ export default ImageDeliveryInsight;
2
+ declare class ImageDeliveryInsight extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
7
+ */
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
+ }
10
+ import { Audit } from '../audit.js';
11
+ //# sourceMappingURL=image-delivery-insight.d.ts.map
@@ -0,0 +1,83 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/ImageDelivery.js';
8
+
9
+ import {Audit} from '../audit.js';
10
+ import * as i18n from '../../lib/i18n/i18n.js';
11
+ import {adaptInsightToAuditProduct} from './insight-audit.js';
12
+
13
+ // eslint-disable-next-line max-len
14
+ const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/ImageDelivery.js', UIStrings);
15
+
16
+ class ImageDeliveryInsight extends Audit {
17
+ /**
18
+ * @return {LH.Audit.Meta}
19
+ */
20
+ static get meta() {
21
+ return {
22
+ id: 'image-delivery-insight',
23
+ title: str_(UIStrings.title),
24
+ failureTitle: str_(UIStrings.title),
25
+ description: str_(UIStrings.description),
26
+ guidanceLevel: 3,
27
+ requiredArtifacts: ['traces', 'TraceElements'],
28
+ replacesAudits: [
29
+ 'modern-image-formats',
30
+ 'uses-optimized-images',
31
+ 'efficient-animated-content',
32
+ 'uses-responsive-images',
33
+ ],
34
+ };
35
+ }
36
+
37
+ /**
38
+ * @param {LH.Artifacts} artifacts
39
+ * @param {LH.Audit.Context} context
40
+ * @return {Promise<LH.Audit.Product>}
41
+ */
42
+ static async audit(artifacts, context) {
43
+ return adaptInsightToAuditProduct(artifacts, context, 'ImageDelivery', (insight) => {
44
+ if (!insight.optimizableImages.length) {
45
+ // TODO: show UIStrings.noOptimizableImages?
46
+ return;
47
+ }
48
+
49
+ const relatedEventsMap = insight.relatedEvents && !Array.isArray(insight.relatedEvents) ?
50
+ insight.relatedEvents :
51
+ null;
52
+
53
+ /** @type {LH.Audit.Details.Table['headings']} */
54
+ const headings = [
55
+ /* eslint-disable max-len */
56
+ {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL), subItemsHeading: {key: 'reason', valueType: 'text'}},
57
+ {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
58
+ {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes), subItemsHeading: {key: 'wastedBytes', valueType: 'bytes'}},
59
+ /* eslint-enable max-len */
60
+ ];
61
+
62
+ /** @type {LH.Audit.Details.Table['items']} */
63
+ const items = insight.optimizableImages.map(image => ({
64
+ url: image.request.args.data.url,
65
+ totalBytes: image.request.args.data.decodedBodyLength,
66
+ wastedBytes: image.byteSavings,
67
+ subItems: {
68
+ type: /** @type {const} */ ('subitems'),
69
+ // TODO: when strings update to remove number from "reason" uistrings, update this
70
+ // to use `image.optimizations.map(...)` and construct strings from the type.
71
+ items: (relatedEventsMap?.get(image.request) ?? []).map((reason, i) => ({
72
+ reason,
73
+ wastedBytes: image.optimizations[i].byteSavings,
74
+ })),
75
+ },
76
+ }));
77
+
78
+ return Audit.makeTableDetails(headings, items);
79
+ });
80
+ }
81
+ }
82
+
83
+ export default ImageDeliveryInsight;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @param {LH.Artifacts} artifacts
3
+ * @param {LH.Audit.Context} context
4
+ * @param {T} insightName
5
+ * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T]) => LH.Audit.Details|undefined} createDetails
6
+ * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
7
+ * @return {Promise<LH.Audit.Product>}
8
+ */
9
+ export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModelsType>(artifacts: LH.Artifacts, context: LH.Audit.Context, insightName: T, createDetails: (insight: import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModels[T]) => LH.Audit.Details | undefined): Promise<LH.Audit.Product>;
10
+ /**
11
+ * @param {LH.Artifacts.TraceElement[]} traceElements
12
+ * @param {number|null|undefined} nodeId
13
+ * @return {LH.Audit.Details.NodeValue|undefined}
14
+ */
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;
23
+ //# sourceMappingURL=insight-audit.d.ts.map
@@ -0,0 +1,133 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {NO_NAVIGATION} from '@paulirish/trace_engine/models/trace/types/TraceEvents.js';
8
+
9
+ import {ProcessedTrace} from '../../computed/processed-trace.js';
10
+ import {TraceEngineResult} from '../../computed/trace-engine-result.js';
11
+ import {Audit} from '../audit.js';
12
+
13
+ /**
14
+ * @param {LH.Artifacts} artifacts
15
+ * @param {LH.Audit.Context} context
16
+ * @return {Promise<import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet|undefined>}
17
+ */
18
+ async function getInsightSet(artifacts, context) {
19
+ const trace = artifacts.traces[Audit.DEFAULT_PASS];
20
+ const processedTrace = await ProcessedTrace.request(trace, context);
21
+ const traceEngineResult = await TraceEngineResult.request({trace}, context);
22
+
23
+ const navigationId = processedTrace.timeOriginEvt.args.data?.navigationId;
24
+ const key = navigationId ?? NO_NAVIGATION;
25
+
26
+ return traceEngineResult.insights.get(key);
27
+ }
28
+
29
+ /**
30
+ * @param {LH.Artifacts} artifacts
31
+ * @param {LH.Audit.Context} context
32
+ * @param {T} insightName
33
+ * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T]) => LH.Audit.Details|undefined} createDetails
34
+ * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
35
+ * @return {Promise<LH.Audit.Product>}
36
+ */
37
+ async function adaptInsightToAuditProduct(artifacts, context, insightName, createDetails) {
38
+ const insights = await getInsightSet(artifacts, context);
39
+ if (!insights) {
40
+ return {
41
+ scoreDisplayMode: Audit.SCORING_MODES.NOT_APPLICABLE,
42
+ score: null,
43
+ };
44
+ }
45
+
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
+
55
+ const details = createDetails(insight);
56
+ if (!details || (details.type === 'table' && details.headings.length === 0)) {
57
+ return {
58
+ scoreDisplayMode: Audit.SCORING_MODES.NOT_APPLICABLE,
59
+ score: null,
60
+ };
61
+ }
62
+
63
+ // This hack is to add metric adorners if an insight category links it to a metric,
64
+ // but doesn't output a metric savings for that metric.
65
+ let metricSavings = insight.metricSavings;
66
+ if (insight.category === 'INP' && !metricSavings?.INP) {
67
+ metricSavings = {...metricSavings, INP: /** @type {any} */ (0)};
68
+ } else if (insight.category === 'CLS' && !metricSavings?.CLS) {
69
+ metricSavings = {...metricSavings, CLS: /** @type {any} */ (0)};
70
+ } else if (insight.category === 'LCP' && !metricSavings?.LCP) {
71
+ metricSavings = {...metricSavings, LCP: /** @type {any} */ (0)};
72
+ }
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
+
82
+ return {
83
+ scoreDisplayMode:
84
+ insight.metricSavings ? Audit.SCORING_MODES.METRIC_SAVINGS : Audit.SCORING_MODES.NUMERIC,
85
+ score,
86
+ metricSavings,
87
+ warnings: insight.warnings,
88
+ details,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * @param {LH.Artifacts.TraceElement[]} traceElements
94
+ * @param {number|null|undefined} nodeId
95
+ * @return {LH.Audit.Details.NodeValue|undefined}
96
+ */
97
+ function makeNodeItemForNodeId(traceElements, nodeId) {
98
+ if (typeof nodeId !== 'number') {
99
+ return;
100
+ }
101
+
102
+ const traceElement =
103
+ traceElements.find(te => te.traceEventType === 'trace-engine' && te.nodeId === nodeId);
104
+ const node = traceElement?.node;
105
+ if (!node) {
106
+ return;
107
+ }
108
+
109
+ return Audit.makeNodeItem(node);
110
+ }
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
+
129
+ export {
130
+ adaptInsightToAuditProduct,
131
+ makeNodeItemForNodeId,
132
+ maybeMakeNodeElementTable,
133
+ };