lighthouse 12.6.1 → 12.7.0-dev.20250627

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 (162) hide show
  1. package/cli/test/smokehouse/core-tests.js +2 -0
  2. package/cli/test/smokehouse/frontends/lib.js +0 -2
  3. package/cli/test/smokehouse/frontends/smokehouse-bin.js +1 -3
  4. package/cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
  5. package/cli/test/smokehouse/smokehouse.js +0 -2
  6. package/core/audits/audit.d.ts +7 -0
  7. package/core/audits/audit.js +17 -2
  8. package/core/audits/byte-efficiency/byte-efficiency-audit.js +6 -8
  9. package/core/audits/byte-efficiency/legacy-javascript.js +0 -1
  10. package/core/audits/byte-efficiency/render-blocking-resources.js +1 -1
  11. package/core/audits/deprecations.js +0 -2
  12. package/core/audits/insights/cache-insight.js +2 -1
  13. package/core/audits/insights/cls-culprits-insight.js +6 -7
  14. package/core/audits/insights/{interaction-to-next-paint-insight.d.ts → inp-breakdown-insight.d.ts} +3 -3
  15. package/core/audits/insights/{interaction-to-next-paint-insight.js → inp-breakdown-insight.js} +10 -10
  16. package/core/audits/insights/insight-audit.d.ts +7 -4
  17. package/core/audits/insights/insight-audit.js +48 -7
  18. package/core/audits/insights/lcp-breakdown-insight.d.ts +16 -0
  19. package/core/audits/insights/{lcp-phases-insight.js → lcp-breakdown-insight.js} +19 -19
  20. package/core/audits/insights/modern-http-insight.js +0 -2
  21. package/core/audits/insights/network-dependency-tree-insight.js +85 -8
  22. package/core/audits/insights/render-blocking-insight.js +1 -1
  23. package/core/audits/layout-shifts.js +5 -4
  24. package/core/audits/seo/crawlable-anchors.js +27 -0
  25. package/core/audits/seo/link-text.js +130 -83
  26. package/core/audits/third-party-cookies.js +0 -2
  27. package/core/audits/valid-source-maps.js +0 -2
  28. package/core/computed/js-bundles.js +0 -1
  29. package/core/computed/metrics/cumulative-layout-shift.js +1 -1
  30. package/core/computed/metrics/lantern-metric.js +4 -3
  31. package/core/computed/metrics/timing-summary.js +4 -1
  32. package/core/computed/page-dependency-graph.js +3 -3
  33. package/core/computed/trace-engine-result.js +1 -2
  34. package/core/config/config.js +1 -1
  35. package/core/config/default-config.js +4 -4
  36. package/core/config/experimental-config.js +2 -2
  37. package/core/config/filters.js +7 -0
  38. package/core/config/validation.js +4 -0
  39. package/core/gather/base-artifacts.js +3 -0
  40. package/core/gather/driver/environment.d.ts +6 -0
  41. package/core/gather/driver/environment.js +17 -0
  42. package/core/gather/driver/execution-context.d.ts +3 -1
  43. package/core/gather/driver/execution-context.js +3 -1
  44. package/core/gather/driver/navigation.js +1 -3
  45. package/core/gather/driver/wait-for-condition.js +0 -1
  46. package/core/gather/fetcher.js +0 -2
  47. package/core/gather/gatherers/accessibility.js +1 -1
  48. package/core/gather/gatherers/anchor-elements.js +61 -2
  49. package/core/gather/gatherers/cache-contents.js +0 -2
  50. package/core/gather/gatherers/css-usage.js +3 -1
  51. package/core/gather/gatherers/dobetterweb/doctype.js +0 -2
  52. package/core/gather/gatherers/dobetterweb/domstats.js +1 -1
  53. package/core/gather/gatherers/full-page-screenshot.js +1 -1
  54. package/core/gather/gatherers/image-elements.js +1 -1
  55. package/core/gather/gatherers/inspector-issues.js +1 -1
  56. package/core/gather/gatherers/link-elements.js +1 -1
  57. package/core/gather/gatherers/stacks.js +0 -1
  58. package/core/gather/gatherers/stylesheets.js +3 -1
  59. package/core/gather/gatherers/trace-elements.d.ts +3 -4
  60. package/core/gather/gatherers/trace-elements.js +13 -43
  61. package/core/gather/gatherers/viewport-dimensions.js +0 -2
  62. package/core/index.cjs +0 -1
  63. package/core/index.d.cts +5 -0
  64. package/core/lib/asset-saver.d.ts +1 -1
  65. package/core/lib/asset-saver.js +20 -8
  66. package/core/lib/bf-cache-strings.js +4 -1
  67. package/core/lib/deprecations-strings.d.ts +51 -47
  68. package/core/lib/deprecations-strings.js +14 -8
  69. package/core/lib/i18n/i18n.js +0 -2
  70. package/core/lib/lantern-trace-saver.js +1 -1
  71. package/core/lib/lh-error.js +0 -1
  72. package/core/lib/manifest-parser.js +0 -2
  73. package/core/lib/minify-devtoolslog.js +0 -2
  74. package/core/lib/sentry.d.ts +1 -1
  75. package/core/lib/sentry.js +2 -2
  76. package/core/runner.js +11 -8
  77. package/core/scoring.d.ts +186 -15
  78. package/core/scripts/manual-chrome-launcher.js +0 -1
  79. package/dist/report/bundle.esm.js +14 -12
  80. package/dist/report/flow.js +18 -16
  81. package/dist/report/standalone.js +15 -13
  82. package/eslint.config.mjs +242 -0
  83. package/flow-report/src/common.tsx +1 -0
  84. package/flow-report/src/i18n/i18n.tsx +1 -0
  85. package/flow-report/src/util.ts +2 -0
  86. package/package.json +23 -19
  87. package/readme.md +3 -2
  88. package/report/assets/styles.css +11 -9
  89. package/report/assets/templates.html +1 -1
  90. package/report/generator/file-namer.d.ts +5 -0
  91. package/report/generator/file-namer.js +1 -1
  92. package/report/generator/flow-report-assets.js +1 -1
  93. package/report/generator/report-assets.js +1 -1
  94. package/report/generator/report-generator.js +3 -3
  95. package/report/renderer/api.js +1 -0
  96. package/report/renderer/components.js +2 -2
  97. package/report/renderer/details-renderer.d.ts +5 -0
  98. package/report/renderer/details-renderer.js +35 -3
  99. package/report/renderer/dom.d.ts +2 -0
  100. package/report/renderer/dom.js +6 -0
  101. package/report/renderer/i18n-formatter.js +2 -1
  102. package/report/renderer/performance-category-renderer.js +2 -2
  103. package/report/renderer/report-renderer.js +1 -0
  104. package/report/renderer/report-ui-features.d.ts +1 -0
  105. package/report/renderer/report-ui-features.js +16 -0
  106. package/report/renderer/report-utils.js +3 -2
  107. package/report/renderer/text-encoding.js +0 -2
  108. package/report/renderer/topbar-features.js +1 -1
  109. package/report/types/report-renderer.d.ts +5 -0
  110. package/shared/localization/locales/ar-XB.json +57 -69
  111. package/shared/localization/locales/ar.json +57 -69
  112. package/shared/localization/locales/bg.json +57 -69
  113. package/shared/localization/locales/ca.json +57 -69
  114. package/shared/localization/locales/cs.json +57 -69
  115. package/shared/localization/locales/da.json +57 -69
  116. package/shared/localization/locales/de.json +57 -69
  117. package/shared/localization/locales/el.json +57 -69
  118. package/shared/localization/locales/en-GB.json +57 -69
  119. package/shared/localization/locales/en-US.json +73 -61
  120. package/shared/localization/locales/en-XL.json +73 -61
  121. package/shared/localization/locales/es-419.json +57 -69
  122. package/shared/localization/locales/es.json +56 -68
  123. package/shared/localization/locales/fi.json +57 -69
  124. package/shared/localization/locales/fil.json +57 -69
  125. package/shared/localization/locales/fr.json +57 -69
  126. package/shared/localization/locales/he.json +57 -69
  127. package/shared/localization/locales/hi.json +57 -69
  128. package/shared/localization/locales/hr.json +57 -69
  129. package/shared/localization/locales/hu.json +56 -68
  130. package/shared/localization/locales/id.json +57 -69
  131. package/shared/localization/locales/it.json +56 -68
  132. package/shared/localization/locales/ja.json +57 -69
  133. package/shared/localization/locales/ko.json +57 -69
  134. package/shared/localization/locales/lt.json +57 -69
  135. package/shared/localization/locales/lv.json +57 -69
  136. package/shared/localization/locales/nl.json +57 -69
  137. package/shared/localization/locales/no.json +57 -69
  138. package/shared/localization/locales/pl.json +56 -68
  139. package/shared/localization/locales/pt-PT.json +57 -69
  140. package/shared/localization/locales/pt.json +57 -69
  141. package/shared/localization/locales/ro.json +57 -69
  142. package/shared/localization/locales/ru.json +58 -70
  143. package/shared/localization/locales/sk.json +57 -69
  144. package/shared/localization/locales/sl.json +56 -68
  145. package/shared/localization/locales/sr-Latn.json +57 -69
  146. package/shared/localization/locales/sr.json +57 -69
  147. package/shared/localization/locales/sv.json +57 -69
  148. package/shared/localization/locales/ta.json +57 -69
  149. package/shared/localization/locales/te.json +57 -69
  150. package/shared/localization/locales/th.json +56 -68
  151. package/shared/localization/locales/tr.json +57 -69
  152. package/shared/localization/locales/uk.json +57 -69
  153. package/shared/localization/locales/vi.json +57 -69
  154. package/shared/localization/locales/zh-HK.json +57 -69
  155. package/shared/localization/locales/zh-TW.json +56 -68
  156. package/shared/localization/locales/zh.json +57 -69
  157. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +1 -1
  158. package/types/artifacts.d.ts +8 -1
  159. package/types/internal/test.d.ts +1 -1
  160. package/types/lhr/audit-details.d.ts +13 -3
  161. package/types/lhr/lhr.d.ts +8 -1
  162. package/core/audits/insights/lcp-phases-insight.d.ts +0 -16
@@ -61,6 +61,7 @@ import screenshot from './test-definitions/screenshot.js';
61
61
  import seoFailing from './test-definitions/seo-failing.js';
62
62
  import seoPassing from './test-definitions/seo-passing.js';
63
63
  import seoStatus403 from './test-definitions/seo-status-403.js';
64
+ import seoMixedLanguage from './test-definitions/seo-mixed-language.js';
64
65
  import serviceWorkerReloaded from './test-definitions/service-worker-reloaded.js';
65
66
  import shiftAttribution from './test-definitions/shift-attribution.js';
66
67
  import sourceMaps from './test-definitions/source-maps.js';
@@ -125,6 +126,7 @@ const smokeTests = [
125
126
  seoFailing,
126
127
  seoPassing,
127
128
  seoStatus403,
129
+ seoMixedLanguage,
128
130
  serviceWorkerReloaded,
129
131
  shiftAttribution,
130
132
  sourceMaps,
@@ -10,8 +10,6 @@
10
10
  * Supports skipping and modifiying expectations to match the environment.
11
11
  */
12
12
 
13
- /* eslint-disable no-console */
14
-
15
13
  import {cloneDeep} from 'lodash-es';
16
14
 
17
15
  import smokeTests from '../core-tests.js';
@@ -10,8 +10,6 @@
10
10
  * flags, start fixture webservers, then run smokehouse.
11
11
  */
12
12
 
13
- /* eslint-disable no-console */
14
-
15
13
  import path from 'path';
16
14
  import fs from 'fs';
17
15
  import url from 'url';
@@ -173,7 +171,7 @@ async function begin() {
173
171
  },
174
172
  'no-headless': {
175
173
  type: 'boolean',
176
- describe: 'Launch Chrome in typical desktop headful mode, rather than our default of `--headless=new` (https://developer.chrome.com/articles/new-headless/).', // eslint-disable-line max-len
174
+ describe: 'Launch Chrome in typical desktop headful mode, rather than our default of `--headless=new` (https://developer.chrome.com/articles/new-headless/).',
177
175
  },
178
176
  })
179
177
  .wrap(y.terminalWidth())
@@ -94,7 +94,7 @@ async function internalRun(url, tmpPath, config, logger, options) {
94
94
 
95
95
  try {
96
96
  await fs.access(outputPath);
97
- } catch (e) {
97
+ } catch {
98
98
  throw new ChildProcessError(`Lighthouse run failed to produce a report.`, logger.getLog());
99
99
  }
100
100
 
@@ -10,8 +10,6 @@
10
10
  * smoke tests passed.
11
11
  */
12
12
 
13
- /* eslint-disable no-console */
14
-
15
13
  /**
16
14
  * @typedef Run
17
15
  * @property {string[] | undefined} networkRequests
@@ -83,6 +83,13 @@ export class Audit {
83
83
  * @return {LH.Audit.Details.List}
84
84
  */
85
85
  static makeListDetails(items: LH.Audit.Details.List["items"]): LH.Audit.Details.List;
86
+ /**
87
+ * @param {LH.IcuMessage | string=} title
88
+ * @param {LH.IcuMessage | string=} description
89
+ * @param {LH.Audit.Details.ListableDetail} value
90
+ * @return {LH.Audit.Details.ListSectionItem}
91
+ */
92
+ static makeListDetailSectionItem(value: LH.Audit.Details.ListableDetail, title?: (LH.IcuMessage | string) | undefined, description?: (LH.IcuMessage | string) | undefined): LH.Audit.Details.ListSectionItem;
86
93
  /** @typedef {{
87
94
  * content: string;
88
95
  * title: string;
@@ -164,7 +164,7 @@ class Audit {
164
164
  if (results.length === 0) {
165
165
  return {
166
166
  type: 'table',
167
- headings: [],
167
+ headings,
168
168
  items: [],
169
169
  summary,
170
170
  };
@@ -174,7 +174,7 @@ class Audit {
174
174
 
175
175
  return {
176
176
  type: 'table',
177
- headings: headings,
177
+ headings,
178
178
  items: results,
179
179
  summary,
180
180
  sortedBy,
@@ -194,6 +194,21 @@ class Audit {
194
194
  };
195
195
  }
196
196
 
197
+ /**
198
+ * @param {LH.IcuMessage | string=} title
199
+ * @param {LH.IcuMessage | string=} description
200
+ * @param {LH.Audit.Details.ListableDetail} value
201
+ * @return {LH.Audit.Details.ListSectionItem}
202
+ */
203
+ static makeListDetailSectionItem(value, title, description) {
204
+ return {
205
+ type: 'list-section',
206
+ title,
207
+ description,
208
+ value,
209
+ };
210
+ }
211
+
197
212
  /** @typedef {{
198
213
  * content: string;
199
214
  * title: string;
@@ -8,8 +8,8 @@ import {Audit} from '../audit.js';
8
8
  import * as i18n from '../../lib/i18n/i18n.js';
9
9
  import {NetworkRecords} from '../../computed/network-records.js';
10
10
  import {LoadSimulator} from '../../computed/load-simulator.js';
11
- import {LanternLargestContentfulPaint} from '../../computed/metrics/lantern-largest-contentful-paint.js';
12
- import {LanternFirstContentfulPaint} from '../../computed/metrics/lantern-first-contentful-paint.js';
11
+ import {LanternLargestContentfulPaint as LanternLCP} from '../../computed/metrics/lantern-largest-contentful-paint.js';
12
+ import {LanternFirstContentfulPaint as LanternFCP} from '../../computed/metrics/lantern-first-contentful-paint.js';
13
13
  import {LCPImageRecord} from '../../computed/lcp-image-record.js';
14
14
 
15
15
  const str_ = i18n.createIcuMessageFn(import.meta.url, {});
@@ -168,12 +168,10 @@ class ByteEfficiencyAudit extends Audit {
168
168
  // This is useful information in the LHR and should be preserved.
169
169
  let wastedMs;
170
170
  if (metricComputationInput.gatherContext.gatherMode === 'navigation') {
171
- const {
172
- optimisticGraph: optimisticFCPGraph,
173
- } = await LanternFirstContentfulPaint.request(metricComputationInput, context);
174
- const {
175
- optimisticGraph: optimisticLCPGraph,
176
- } = await LanternLargestContentfulPaint.request(metricComputationInput, context);
171
+ const optimisticFCPGraph = (await LanternFCP.request(metricComputationInput, context))
172
+ .optimisticGraph;
173
+ const optimisticLCPGraph = (await LanternLCP.request(metricComputationInput, context))
174
+ .optimisticGraph;
177
175
 
178
176
  const {savings: fcpSavings} = this.computeWasteWithGraph(
179
177
  results,
@@ -25,7 +25,6 @@ import {detectLegacyJavaScript} from '../../lib/legacy-javascript/legacy-javascr
25
25
  const UIStrings = {
26
26
  /** Title of a Lighthouse audit that tells the user about legacy polyfills and transforms used on the page. This is displayed in a list of audit titles that Lighthouse generates. */
27
27
  title: 'Avoid serving legacy JavaScript to modern browsers',
28
- // eslint-disable-next-line max-len
29
28
  // TODO: developer.chrome.com article. this codelab is good starting place: https://web.dev/articles/codelab-serve-modern-code
30
29
  /** Description of a Lighthouse audit that tells the user about old JavaScript that is no longer needed. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
31
30
  description: 'Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren\'t necessary for modern browsers. Consider modifying your JavaScript build process to not transpile [Baseline](https://web.dev/baseline) features, unless you know you must support legacy browsers. [Learn why most sites can deploy ES6+ code without transpiling](https://philipwalton.com/articles/the-state-of-es5-on-the-web/)',
@@ -291,7 +291,7 @@ class RenderBlockingResources extends Audit {
291
291
  const headings = [
292
292
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
293
293
  {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize)},
294
- {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
294
+ {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnDuration)},
295
295
  ];
296
296
 
297
297
  const details = Audit.makeOpportunityDetails(headings, results,
@@ -13,7 +13,6 @@ import {JSBundles} from '../computed/js-bundles.js';
13
13
  import * as i18n from '../lib/i18n/i18n.js';
14
14
  import {getIssueDetailDescription} from '../lib/deprecation-description.js';
15
15
 
16
- /* eslint-disable max-len */
17
16
  const UIStrings = {
18
17
  /** Title of a Lighthouse audit that provides detail on the use of deprecated APIs. This descriptive title is shown to users when the page does not use deprecated APIs. */
19
18
  title: 'Avoids deprecated APIs',
@@ -32,7 +31,6 @@ const UIStrings = {
32
31
  /** Table column header for line of code (eg. 432) that is using a deprecated API. */
33
32
  columnLine: 'Line',
34
33
  };
35
- /* eslint-enable max-len */
36
34
 
37
35
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
38
36
 
@@ -41,7 +41,7 @@ class CacheInsight extends Audit {
41
41
  /* eslint-disable max-len */
42
42
  {key: 'url', valueType: 'url', label: str_(UIStrings.requestColumn)},
43
43
  {key: 'cacheLifetimeMs', valueType: 'ms', label: str_(UIStrings.cacheTTL), displayUnit: 'duration'},
44
- {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize), displayUnit: 'kb', granularity: 1},
44
+ {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize), displayUnit: 'kb', granularity: 1},
45
45
  /* eslint-enable max-len */
46
46
  ];
47
47
  // TODO: this should be the sorting in the model (instead it sorts by transfer size...)
@@ -50,6 +50,7 @@ class CacheInsight extends Audit {
50
50
  const items = values.map(value => ({
51
51
  url: value.request.args.data.url,
52
52
  cacheLifetimeMs: value.ttl * 1000,
53
+ totalBytes: value.request.args.data.encodedDataLength || 0,
53
54
  wastedBytes: value.wastedBytes,
54
55
  }));
55
56
  return Audit.makeTableDetails(headings, items, {
@@ -19,12 +19,10 @@ const MAX_LAYOUT_SHIFTS_PER_CLUSTER = 5;
19
19
  // eslint-disable-next-line max-len
20
20
  const insightStr_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/CLSCulprits.js', InsightUIStrings);
21
21
 
22
- /* eslint-disable max-len */
23
22
  const UIStrings = {
24
23
  /** Label for a column in a data table; entries in this column will be a number representing how large the layout shift was. */
25
24
  columnScore: 'Layout shift score',
26
25
  };
27
- /* eslint-enable max-len */
28
26
 
29
27
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
30
28
 
@@ -61,18 +59,19 @@ class CLSCulpritsInsight extends Audit {
61
59
  for (const unsizedImage of culprits.unsizedImages) {
62
60
  subItems.push({
63
61
  extra: makeNodeItemForNodeId(TraceElements, unsizedImage.backendNodeId),
64
- cause: insightStr_(InsightUIStrings.unsizedImages),
62
+ cause: insightStr_(InsightUIStrings.unsizedImage),
65
63
  });
66
64
  }
67
- for (const request of culprits.fontRequests) {
65
+ for (const request of culprits.webFonts) {
68
66
  const url = request.args.data.url;
69
67
  subItems.push({
70
68
  extra: {type: 'url', value: url},
71
- cause: insightStr_(InsightUIStrings.fontRequest),
69
+ cause: insightStr_(InsightUIStrings.webFont),
72
70
  });
73
71
  }
74
- if (culprits.iframeIds.length) {
72
+ for (const iframe of culprits.iframes) {
75
73
  subItems.push({
74
+ extra: iframe.url ? {type: 'url', value: iframe.url} : undefined,
76
75
  cause: insightStr_(InsightUIStrings.injectedIframe),
77
76
  });
78
77
  }
@@ -114,7 +113,7 @@ class CLSCulpritsInsight extends Audit {
114
113
  /** @type {LH.Audit.Details.Table['items']} */
115
114
  const items = events.map(event => {
116
115
  const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
117
- event.args.data.impacted_nodes || [], impactByNodeId, event);
116
+ event.args.data.impacted_nodes || [], impactByNodeId);
118
117
  return {
119
118
  node: makeNodeItemForNodeId(artifacts.TraceElements, biggestImpactNodeId),
120
119
  score: event.args.data?.weighted_score_delta,
@@ -1,5 +1,5 @@
1
- export default InteractionToNextPaintInsight;
2
- declare class InteractionToNextPaintInsight extends Audit {
1
+ export default INPBreakdownInsight;
2
+ declare class INPBreakdownInsight extends Audit {
3
3
  /**
4
4
  * @param {LH.Artifacts} artifacts
5
5
  * @param {LH.Audit.Context} context
@@ -8,4 +8,4 @@ declare class InteractionToNextPaintInsight extends Audit {
8
8
  static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
9
  }
10
10
  import { Audit } from '../audit.js';
11
- //# sourceMappingURL=interaction-to-next-paint-insight.d.ts.map
11
+ //# sourceMappingURL=inp-breakdown-insight.d.ts.map
@@ -4,22 +4,22 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/InteractionToNextPaint.js';
7
+ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/INPBreakdown.js';
8
8
 
9
9
  import {Audit} from '../audit.js';
10
10
  import * as i18n from '../../lib/i18n/i18n.js';
11
11
  import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
12
12
 
13
13
  // eslint-disable-next-line max-len
14
- const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/InteractionToNextPaint.js', UIStrings);
14
+ const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/INPBreakdown.js', UIStrings);
15
15
 
16
- class InteractionToNextPaintInsight extends Audit {
16
+ class INPBreakdownInsight extends Audit {
17
17
  /**
18
18
  * @return {LH.Audit.Meta}
19
19
  */
20
20
  static get meta() {
21
21
  return {
22
- id: 'interaction-to-next-paint-insight',
22
+ id: 'inp-breakdown-insight',
23
23
  title: str_(UIStrings.title),
24
24
  failureTitle: str_(UIStrings.title),
25
25
  description: str_(UIStrings.description),
@@ -35,7 +35,7 @@ class InteractionToNextPaintInsight extends Audit {
35
35
  * @return {Promise<LH.Audit.Product>}
36
36
  */
37
37
  static async audit(artifacts, context) {
38
- return adaptInsightToAuditProduct(artifacts, context, 'InteractionToNextPaint', (insight) => {
38
+ return adaptInsightToAuditProduct(artifacts, context, 'INPBreakdown', (insight) => {
39
39
  const event = insight.longestInteractionEvent;
40
40
  if (!event) {
41
41
  // TODO: show UIStrings.noInteractions?
@@ -44,16 +44,16 @@ class InteractionToNextPaintInsight extends Audit {
44
44
 
45
45
  /** @type {LH.Audit.Details.Table['headings']} */
46
46
  const headings = [
47
- {key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
47
+ {key: 'label', valueType: 'text', label: str_(UIStrings.subpart)},
48
48
  {key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
49
49
  ];
50
50
 
51
51
  /** @type {LH.Audit.Details.Table['items']} */
52
52
  const items = [
53
53
  /* eslint-disable max-len */
54
- {phase: 'inputDelay', label: str_(UIStrings.inputDelay), duration: event.inputDelay / 1000},
55
- {phase: 'processingDuration', label: str_(UIStrings.processingDuration), duration: event.mainThreadHandling / 1000},
56
- {phase: 'presentationDelay', label: str_(UIStrings.presentationDelay), duration: event.presentationDelay / 1000},
54
+ {subpart: 'inputDelay', label: str_(UIStrings.inputDelay), duration: event.inputDelay / 1000},
55
+ {subpart: 'processingDuration', label: str_(UIStrings.processingDuration), duration: event.mainThreadHandling / 1000},
56
+ {subpart: 'presentationDelay', label: str_(UIStrings.presentationDelay), duration: event.presentationDelay / 1000},
57
57
  /* eslint-enable max-len */
58
58
  ];
59
59
 
@@ -65,4 +65,4 @@ class InteractionToNextPaintInsight extends Audit {
65
65
  }
66
66
  }
67
67
 
68
- export default InteractionToNextPaintInsight;
68
+ export default INPBreakdownInsight;
@@ -1,21 +1,24 @@
1
1
  export type CreateDetailsExtras = {
2
2
  insights: import("@paulirish/trace_engine/models/trace/insights/types.js").InsightSet;
3
- parsedTrace: LH.Artifacts.TraceEngineResult["data"];
3
+ parsedTrace: LH.Artifacts.TraceEngineResult["parsedTrace"];
4
4
  };
5
5
  /**
6
6
  * @typedef CreateDetailsExtras
7
7
  * @property {import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet} insights
8
- * @property {LH.Artifacts.TraceEngineResult['data']} parsedTrace
8
+ * @property {LH.Artifacts.TraceEngineResult['parsedTrace']} parsedTrace
9
9
  */
10
10
  /**
11
11
  * @param {LH.Artifacts} artifacts
12
12
  * @param {LH.Audit.Context} context
13
13
  * @param {T} insightName
14
- * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => LH.Audit.Details|undefined} createDetails
14
+ * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings: Array<string | LH.IcuMessage>}|LH.Audit.Details|undefined} createDetails
15
15
  * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
16
16
  * @return {Promise<LH.Audit.Product>}
17
17
  */
18
- 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], extras: CreateDetailsExtras) => LH.Audit.Details | undefined): Promise<LH.Audit.Product>;
18
+ 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], extras: CreateDetailsExtras) => {
19
+ details: LH.Audit.Details;
20
+ warnings: Array<string | LH.IcuMessage>;
21
+ } | LH.Audit.Details | undefined): Promise<LH.Audit.Product>;
19
22
  /**
20
23
  * @param {LH.Artifacts.TraceElement[]} traceElements
21
24
  * @param {number|null|undefined} nodeId
@@ -16,7 +16,7 @@ const str_ = i18n.createIcuMessageFn(import.meta.url, {});
16
16
  /**
17
17
  * @param {LH.Artifacts} artifacts
18
18
  * @param {LH.Audit.Context} context
19
- * @return {Promise<{insights: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet|undefined, parsedTrace: LH.Artifacts.TraceEngineResult['data']}>}
19
+ * @return {Promise<{insights: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet|undefined, parsedTrace: LH.Artifacts.TraceEngineResult['parsedTrace']}>}
20
20
  */
21
21
  async function getInsightSet(artifacts, context) {
22
22
  const settings = context.settings;
@@ -29,20 +29,20 @@ async function getInsightSet(artifacts, context) {
29
29
  const key = navigationId ?? NO_NAVIGATION;
30
30
  const insights = traceEngineResult.insights.get(key);
31
31
 
32
- return {insights, parsedTrace: traceEngineResult.data};
32
+ return {insights, parsedTrace: traceEngineResult.parsedTrace};
33
33
  }
34
34
 
35
35
  /**
36
36
  * @typedef CreateDetailsExtras
37
37
  * @property {import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet} insights
38
- * @property {LH.Artifacts.TraceEngineResult['data']} parsedTrace
38
+ * @property {LH.Artifacts.TraceEngineResult['parsedTrace']} parsedTrace
39
39
  */
40
40
 
41
41
  /**
42
42
  * @param {LH.Artifacts} artifacts
43
43
  * @param {LH.Audit.Context} context
44
44
  * @param {T} insightName
45
- * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => LH.Audit.Details|undefined} createDetails
45
+ * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings: Array<string | LH.IcuMessage>}|LH.Audit.Details|undefined} createDetails
46
46
  * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
47
47
  * @return {Promise<LH.Audit.Product>}
48
48
  */
@@ -64,14 +64,26 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
64
64
  };
65
65
  }
66
66
 
67
- const details = createDetails(insight, {
67
+ const cbResult = createDetails(insight, {
68
68
  parsedTrace,
69
69
  insights,
70
70
  });
71
- if (!details || (details.type === 'table' && details.headings.length === 0)) {
71
+
72
+ const warnings = [...insight.warnings ?? []];
73
+
74
+ let details;
75
+ if (cbResult && 'warnings' in cbResult) {
76
+ details = cbResult.details;
77
+ warnings.push(...cbResult.warnings);
78
+ } else {
79
+ details = cbResult;
80
+ }
81
+
82
+ if (!details || (details.type === 'table' && details.items.length === 0)) {
72
83
  return {
73
84
  scoreDisplayMode: Audit.SCORING_MODES.NOT_APPLICABLE,
74
85
  score: null,
86
+ details,
75
87
  };
76
88
  }
77
89
 
@@ -96,8 +108,37 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
96
108
  // TODO: consider adding a `estimatedSavingsText` to InsightModel, which can capture
97
109
  // the exact i18n string used by RPP; and include the same est. timing savings.
98
110
  let displayValue;
111
+
99
112
  if (insight.wastedBytes) {
100
113
  displayValue = str_(i18n.UIStrings.displayValueByteSavings, {wastedBytes: insight.wastedBytes});
114
+ } else {
115
+ let wastedMs;
116
+
117
+ switch (insight.insightKey) {
118
+ case 'DocumentLatency':
119
+ case 'DuplicatedJavaScript':
120
+ case 'FontDisplay':
121
+ case 'LegacyJavaScript':
122
+ case 'RenderBlocking': {
123
+ wastedMs = metricSavings?.FCP;
124
+ break;
125
+ }
126
+
127
+ case 'LCPDiscovery':
128
+ case 'ModernHTTP': {
129
+ wastedMs = metricSavings?.LCP;
130
+ break;
131
+ }
132
+
133
+ case 'Viewport': {
134
+ wastedMs = metricSavings?.INP;
135
+ break;
136
+ }
137
+ }
138
+
139
+ if (wastedMs) {
140
+ displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs});
141
+ }
101
142
  }
102
143
 
103
144
  let score;
@@ -115,7 +156,7 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
115
156
  scoreDisplayMode,
116
157
  score,
117
158
  metricSavings,
118
- warnings: insight.warnings,
159
+ warnings: warnings.length ? warnings : undefined,
119
160
  displayValue,
120
161
  details,
121
162
  };
@@ -0,0 +1,16 @@
1
+ export default LCPBreakdownInsight;
2
+ declare class LCPBreakdownInsight extends Audit {
3
+ /**
4
+ * @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPBreakdown.js').LCPBreakdownInsightModel>['subparts']} subparts
5
+ * @return {LH.Audit.Details.Table}
6
+ */
7
+ static makeSubpartsTable(subparts: Required<import("@paulirish/trace_engine/models/trace/insights/LCPBreakdown.js").LCPBreakdownInsightModel>["subparts"]): LH.Audit.Details.Table;
8
+ /**
9
+ * @param {LH.Artifacts} artifacts
10
+ * @param {LH.Audit.Context} context
11
+ * @return {Promise<LH.Audit.Product>}
12
+ */
13
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
14
+ }
15
+ import { Audit } from '../audit.js';
16
+ //# sourceMappingURL=lcp-breakdown-insight.d.ts.map
@@ -4,22 +4,22 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/LCPPhases.js';
7
+ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/LCPBreakdown.js';
8
8
 
9
9
  import {Audit} from '../audit.js';
10
10
  import * as i18n from '../../lib/i18n/i18n.js';
11
11
  import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
12
12
 
13
13
  // eslint-disable-next-line max-len
14
- const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/LCPPhases.js', UIStrings);
14
+ const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/LCPBreakdown.js', UIStrings);
15
15
 
16
- class LCPPhasesInsight extends Audit {
16
+ class LCPBreakdownInsight extends Audit {
17
17
  /**
18
18
  * @return {LH.Audit.Meta}
19
19
  */
20
20
  static get meta() {
21
21
  return {
22
- id: 'lcp-phases-insight',
22
+ id: 'lcp-breakdown-insight',
23
23
  title: str_(UIStrings.title),
24
24
  failureTitle: str_(UIStrings.title),
25
25
  description: str_(UIStrings.description),
@@ -30,33 +30,33 @@ class LCPPhasesInsight extends Audit {
30
30
  }
31
31
 
32
32
  /**
33
- * @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPPhases.js').LCPPhasesInsightModel>['phases']} phases
33
+ * @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPBreakdown.js').LCPBreakdownInsightModel>['subparts']} subparts
34
34
  * @return {LH.Audit.Details.Table}
35
35
  */
36
- static makePhaseTable(phases) {
37
- const {ttfb, loadDelay, loadTime, renderDelay} = phases;
36
+ static makeSubpartsTable(subparts) {
37
+ const {ttfb, loadDelay, loadDuration, renderDelay} = subparts;
38
38
 
39
39
  /** @type {LH.Audit.Details.Table['headings']} */
40
40
  const headings = [
41
- {key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
41
+ {key: 'label', valueType: 'text', label: str_(UIStrings.subpart)},
42
42
  {key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
43
43
  ];
44
44
 
45
45
  /** @type {LH.Audit.Details.Table['items']} */
46
46
  let items = [
47
47
  /* eslint-disable max-len */
48
- {phase: 'timeToFirstByte', label: str_(UIStrings.timeToFirstByte), duration: ttfb},
49
- {phase: 'resourceLoadDelay', label: str_(UIStrings.resourceLoadDelay), duration: loadDelay},
50
- {phase: 'resourceLoadDuration', label: str_(UIStrings.resourceLoadDuration), duration: loadTime},
51
- {phase: 'elementRenderDelay', label: str_(UIStrings.elementRenderDelay), duration: renderDelay},
48
+ {subpart: 'timeToFirstByte', label: str_(UIStrings.timeToFirstByte), duration: ttfb.range / 1000},
49
+ {subpart: 'resourceLoadDelay', label: str_(UIStrings.resourceLoadDelay), duration: (loadDelay?.range ?? 0) / 1000},
50
+ {subpart: 'resourceLoadDuration', label: str_(UIStrings.resourceLoadDuration), duration: (loadDuration?.range ?? 0) / 1000},
51
+ {subpart: 'elementRenderDelay', label: str_(UIStrings.elementRenderDelay), duration: renderDelay.range / 1000},
52
52
  /* eslint-enable max-len */
53
53
  ];
54
54
 
55
55
  if (loadDelay === undefined) {
56
- items = items.filter(item => item.phase !== 'resourceLoadDelay');
56
+ items = items.filter(item => item.subpart !== 'resourceLoadDelay');
57
57
  }
58
- if (loadTime === undefined) {
59
- items = items.filter(item => item.phase !== 'resourceLoadDuration');
58
+ if (loadDuration === undefined) {
59
+ items = items.filter(item => item.subpart !== 'resourceLoadDuration');
60
60
  }
61
61
 
62
62
  return Audit.makeTableDetails(headings, items);
@@ -68,17 +68,17 @@ class LCPPhasesInsight extends Audit {
68
68
  * @return {Promise<LH.Audit.Product>}
69
69
  */
70
70
  static async audit(artifacts, context) {
71
- return adaptInsightToAuditProduct(artifacts, context, 'LCPPhases', (insight) => {
72
- if (!insight.phases) {
71
+ return adaptInsightToAuditProduct(artifacts, context, 'LCPBreakdown', (insight) => {
72
+ if (!insight.subparts) {
73
73
  return;
74
74
  }
75
75
 
76
76
  return Audit.makeListDetails([
77
- LCPPhasesInsight.makePhaseTable(insight.phases),
77
+ LCPBreakdownInsight.makeSubpartsTable(insight.subparts),
78
78
  makeNodeItemForNodeId(artifacts.TraceElements, insight.lcpEvent?.args.data?.nodeId),
79
79
  ].filter(table => table !== undefined));
80
80
  });
81
81
  }
82
82
  }
83
83
 
84
- export default LCPPhasesInsight;
84
+ export default LCPBreakdownInsight;
@@ -37,10 +37,8 @@ class ModernHTTPInsight extends Audit {
37
37
  return adaptInsightToAuditProduct(artifacts, context, 'ModernHTTP', (insight) => {
38
38
  /** @type {LH.Audit.Details.Table['headings']} */
39
39
  const headings = [
40
- /* eslint-disable max-len */
41
40
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
42
41
  {key: 'protocol', valueType: 'text', label: str_(UIStrings.protocol)},
43
- /* eslint-enable max-len */
44
42
  ];
45
43
  /** @type {LH.Audit.Details.Table['items']} */
46
44
  const items =