lighthouse 13.5.0 → 13.6.0-dev.20261027

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 (266) hide show
  1. package/.agents/skills/update-ard-port/SKILL.md +171 -0
  2. package/.agents/skills/update-trace-engine/SKILL.md +154 -0
  3. package/cli/bin.d.ts +2 -2
  4. package/cli/bin.js +5 -4
  5. package/cli/cli-flags.d.ts +16 -1
  6. package/cli/cli-flags.js +53 -2
  7. package/cli/printer.js +5 -8
  8. package/cli/run.d.ts +5 -5
  9. package/cli/run.js +97 -47
  10. package/cli/sentry-prompt.js +3 -4
  11. package/cli/test/smokehouse/__snapshots__/report-assert-test.js.snap +1 -1
  12. package/cli/test/smokehouse/config/exclusions.js +3 -1
  13. package/cli/test/smokehouse/frontends/smokehouse-bin.js +5 -3
  14. package/cli/test/smokehouse/report-assert-test.js +65 -0
  15. package/cli/test/smokehouse/report-assert.js +24 -39
  16. package/cli/test/smokehouse/smokehouse-test.d.ts +2 -0
  17. package/cli/test/smokehouse/smokehouse-test.js +120 -0
  18. package/cli/test/smokehouse/smokehouse.js +37 -15
  19. package/cli/test/smokehouse/version-check-test.js +5 -0
  20. package/cli/test/smokehouse/version-check.d.ts +1 -1
  21. package/cli/test/smokehouse/version-check.js +1 -1
  22. package/commitlint.config.js +1 -1
  23. package/core/audits/accessibility/accesskeys.js +1 -1
  24. package/core/audits/accessibility/aria-allowed-attr.js +1 -1
  25. package/core/audits/accessibility/aria-allowed-role.js +1 -1
  26. package/core/audits/accessibility/aria-command-name.js +1 -1
  27. package/core/audits/accessibility/aria-conditional-attr.js +1 -1
  28. package/core/audits/accessibility/aria-deprecated-role.js +1 -1
  29. package/core/audits/accessibility/aria-dialog-name.js +1 -1
  30. package/core/audits/accessibility/aria-hidden-body.js +1 -1
  31. package/core/audits/accessibility/aria-hidden-focus.js +1 -1
  32. package/core/audits/accessibility/aria-input-field-name.js +1 -1
  33. package/core/audits/accessibility/aria-meter-name.js +1 -1
  34. package/core/audits/accessibility/aria-progressbar-name.js +1 -1
  35. package/core/audits/accessibility/aria-prohibited-attr.js +1 -1
  36. package/core/audits/accessibility/aria-required-attr.js +1 -1
  37. package/core/audits/accessibility/aria-required-children.js +1 -1
  38. package/core/audits/accessibility/aria-required-parent.js +1 -1
  39. package/core/audits/accessibility/aria-roles.js +1 -1
  40. package/core/audits/accessibility/aria-text.js +1 -1
  41. package/core/audits/accessibility/aria-toggle-field-name.js +1 -1
  42. package/core/audits/accessibility/aria-tooltip-name.js +1 -1
  43. package/core/audits/accessibility/aria-treeitem-name.js +1 -1
  44. package/core/audits/accessibility/aria-valid-attr-value.js +1 -1
  45. package/core/audits/accessibility/aria-valid-attr.js +1 -1
  46. package/core/audits/accessibility/autocomplete-valid.js +1 -1
  47. package/core/audits/accessibility/button-name.js +1 -1
  48. package/core/audits/accessibility/bypass.js +1 -1
  49. package/core/audits/accessibility/color-contrast.js +1 -1
  50. package/core/audits/accessibility/definition-list.js +1 -1
  51. package/core/audits/accessibility/dlitem.js +1 -1
  52. package/core/audits/accessibility/document-title.js +1 -1
  53. package/core/audits/accessibility/duplicate-id-aria.js +1 -1
  54. package/core/audits/accessibility/empty-heading.js +1 -1
  55. package/core/audits/accessibility/form-field-multiple-labels.js +1 -1
  56. package/core/audits/accessibility/frame-title.js +1 -1
  57. package/core/audits/accessibility/heading-order.js +1 -1
  58. package/core/audits/accessibility/html-has-lang.js +1 -1
  59. package/core/audits/accessibility/html-lang-valid.js +1 -1
  60. package/core/audits/accessibility/html-xml-lang-mismatch.js +1 -1
  61. package/core/audits/accessibility/identical-links-same-purpose.js +1 -1
  62. package/core/audits/accessibility/image-alt.js +1 -1
  63. package/core/audits/accessibility/image-redundant-alt.js +1 -1
  64. package/core/audits/accessibility/input-button-name.js +1 -1
  65. package/core/audits/accessibility/input-image-alt.js +1 -1
  66. package/core/audits/accessibility/label-content-name-mismatch.js +1 -1
  67. package/core/audits/accessibility/label.js +1 -1
  68. package/core/audits/accessibility/landmark-one-main.js +1 -1
  69. package/core/audits/accessibility/link-in-text-block.js +1 -1
  70. package/core/audits/accessibility/link-name.js +1 -1
  71. package/core/audits/accessibility/list.js +1 -1
  72. package/core/audits/accessibility/listitem.js +1 -1
  73. package/core/audits/accessibility/meta-refresh.js +1 -1
  74. package/core/audits/accessibility/meta-viewport.js +1 -1
  75. package/core/audits/accessibility/object-alt.js +1 -1
  76. package/core/audits/accessibility/presentation-role-conflict.js +1 -1
  77. package/core/audits/accessibility/select-name.js +1 -1
  78. package/core/audits/accessibility/skip-link.js +1 -1
  79. package/core/audits/accessibility/svg-img-alt.js +1 -1
  80. package/core/audits/accessibility/tabindex.js +1 -1
  81. package/core/audits/accessibility/table-duplicate-name.js +1 -1
  82. package/core/audits/accessibility/table-fake-caption.js +1 -1
  83. package/core/audits/accessibility/target-size.js +1 -1
  84. package/core/audits/accessibility/td-has-header.js +1 -1
  85. package/core/audits/accessibility/td-headers-attr.js +1 -1
  86. package/core/audits/accessibility/th-has-data-cells.js +1 -1
  87. package/core/audits/accessibility/valid-lang.js +1 -1
  88. package/core/audits/accessibility/video-caption.js +1 -1
  89. package/core/audits/agentic/ard-schema.d.ts +13 -0
  90. package/core/audits/agentic/ard-schema.js +81 -33
  91. package/core/audits/agentic/llms-txt.js +5 -2
  92. package/core/audits/baseline.d.ts +29 -7
  93. package/core/audits/baseline.js +19 -6
  94. package/core/audits/byte-efficiency/byte-efficiency-audit.js +2 -2
  95. package/core/audits/insights/insight-audit.d.ts +9 -0
  96. package/core/audits/insights/insight-audit.js +35 -28
  97. package/core/audits/long-tasks.js +2 -1
  98. package/core/audits/predictive-perf.js +2 -2
  99. package/core/audits/screenshot-thumbnails.d.ts +0 -13
  100. package/core/audits/screenshot-thumbnails.js +0 -45
  101. package/core/audits/script-treemap-data.js +26 -6
  102. package/core/audits/seo/robots-txt.js +3 -8
  103. package/core/audits/unsized-images.d.ts +0 -7
  104. package/core/audits/unsized-images.js +7 -26
  105. package/core/computed/lcp-image-record.d.ts +2 -23
  106. package/core/computed/lcp-image-record.js +3 -67
  107. package/core/computed/lcp-network-record.d.ts +24 -0
  108. package/core/computed/lcp-network-record.js +74 -0
  109. package/core/computed/metrics/lantern-metric.js +1 -1
  110. package/core/computed/metrics/lcp-breakdown.js +6 -6
  111. package/core/computed/metrics/time-to-first-byte.js +5 -3
  112. package/core/computed/navigation-insights.js +3 -2
  113. package/core/computed/page-dependency-graph.js +55 -5
  114. package/core/computed/speedline.js +20 -5
  115. package/core/computed/trace-engine-result.js +25 -5
  116. package/core/computed/unused-css.js +15 -2
  117. package/core/config/config-helpers.d.ts +5 -0
  118. package/core/config/config-helpers.js +8 -28
  119. package/core/config/config.js +1 -2
  120. package/core/config/constants.d.ts +2 -28
  121. package/core/config/lr-desktop-config.js +13 -0
  122. package/core/config/lr-mobile-config.js +13 -0
  123. package/core/gather/base-artifacts.js +7 -1
  124. package/core/gather/driver/storage.js +3 -2
  125. package/core/gather/gatherers/agentic/ard.d.ts +59 -4
  126. package/core/gather/gatherers/agentic/ard.js +218 -56
  127. package/core/gather/gatherers/full-page-screenshot.js +2 -2
  128. package/core/gather/gatherers/image-elements.d.ts +7 -0
  129. package/core/gather/gatherers/image-elements.js +75 -24
  130. package/core/gather/gatherers/inspector-issues.js +2 -0
  131. package/core/gather/gatherers/js-usage.js +2 -0
  132. package/core/gather/gatherers/scripts.d.ts +16 -3
  133. package/core/gather/gatherers/scripts.js +157 -14
  134. package/core/gather/gatherers/stylesheets.js +2 -0
  135. package/core/gather/gatherers/trace-elements.js +2 -1
  136. package/core/gather/gatherers/trace.d.ts +6 -0
  137. package/core/gather/gatherers/trace.js +56 -16
  138. package/core/lib/arbitrary-equality-map.d.ts +14 -3
  139. package/core/lib/arbitrary-equality-map.js +113 -6
  140. package/core/lib/baseline/web-features-data.json +32 -11
  141. package/core/lib/baseline/web-features-metadata.json +1 -1
  142. package/core/lib/bf-cache-strings.js +237 -254
  143. package/core/lib/cdt/Common.d.ts +1 -1
  144. package/core/lib/cdt/Common.js +2 -2
  145. package/core/lib/cdt/Platform.d.ts +7 -7
  146. package/core/lib/cdt/Platform.js +11 -8
  147. package/core/lib/cdt/SDK.d.ts +5 -1
  148. package/core/lib/cdt/SDK.js +4 -2
  149. package/core/lib/cdt/generated/ParsedURL.d.ts +0 -1
  150. package/core/lib/cdt/generated/ParsedURL.js +3 -9
  151. package/core/lib/cdt/generated/SourceMap.d.ts +24 -25
  152. package/core/lib/cdt/generated/SourceMap.js +7 -15
  153. package/core/lib/deprecations-strings.d.ts +93 -88
  154. package/core/lib/deprecations-strings.js +20 -14
  155. package/core/lib/i18n/i18n.d.ts +0 -1
  156. package/core/lib/i18n/i18n.js +17 -11
  157. package/core/lib/legacy-javascript/legacy-javascript.d.ts +3 -2
  158. package/core/lib/legacy-javascript/legacy-javascript.js +235 -111
  159. package/core/lib/legacy-javascript/polyfill-graph-data.json +94 -89
  160. package/core/lib/legacy-javascript/polyfill-module-data.json +48 -7
  161. package/core/lib/minification-estimator.js +10 -10
  162. package/core/lib/network-request.d.ts +2 -0
  163. package/core/lib/network-request.js +21 -0
  164. package/core/lib/page-functions.js +2 -5
  165. package/core/lib/sentry.d.ts +1 -0
  166. package/core/lib/sentry.js +3 -0
  167. package/core/lib/trace-engine.d.ts +1 -0
  168. package/core/lib/trace-engine.js +2 -0
  169. package/core/runner.js +9 -3
  170. package/dist/report/bundle.esm.js +10 -10
  171. package/dist/report/flow.js +13 -13
  172. package/dist/report/standalone.js +5 -5
  173. package/flow-report/src/common.tsx +5 -2
  174. package/flow-report/src/i18n/i18n.d.ts +2 -0
  175. package/flow-report/src/summary/summary.tsx +9 -1
  176. package/flow-report/src/util.d.ts +1 -1
  177. package/package.json +62 -98
  178. package/readme.md +13 -18
  179. package/report/generator/file-namer.d.ts +1 -1
  180. package/report/generator/file-namer.js +5 -4
  181. package/report/generator/flow-report-assets.js +1 -3
  182. package/report/generator/report-assets.js +1 -2
  183. package/report/renderer/category-renderer.js +2 -2
  184. package/report/renderer/details-renderer.js +3 -2
  185. package/report/renderer/dom.d.ts +8 -2
  186. package/report/renderer/dom.js +29 -1
  187. package/report/renderer/element-screenshot-renderer.js +3 -0
  188. package/report/renderer/performance-category-renderer.js +3 -1
  189. package/report/renderer/report-renderer.js +4 -0
  190. package/report/renderer/report-utils.d.ts +1 -0
  191. package/report/renderer/report-utils.js +2 -0
  192. package/report/renderer/text-encoding.js +2 -2
  193. package/report/types/augment-dom.d.ts +4 -0
  194. package/shared/esm-utils.d.ts +0 -8
  195. package/shared/esm-utils.js +0 -18
  196. package/shared/localization/format.js +1 -2
  197. package/shared/localization/locales/ar-XB.json +157 -142
  198. package/shared/localization/locales/ar.json +157 -142
  199. package/shared/localization/locales/bg.json +154 -139
  200. package/shared/localization/locales/ca.json +157 -142
  201. package/shared/localization/locales/cs.json +148 -133
  202. package/shared/localization/locales/da.json +153 -138
  203. package/shared/localization/locales/de.json +152 -137
  204. package/shared/localization/locales/el.json +153 -138
  205. package/shared/localization/locales/en-GB.json +160 -145
  206. package/shared/localization/locales/en-US.json +239 -218
  207. package/shared/localization/locales/en-XL.json +239 -218
  208. package/shared/localization/locales/es-419.json +150 -135
  209. package/shared/localization/locales/es.json +149 -134
  210. package/shared/localization/locales/fi.json +151 -136
  211. package/shared/localization/locales/fil.json +154 -139
  212. package/shared/localization/locales/fr.json +150 -135
  213. package/shared/localization/locales/he.json +160 -145
  214. package/shared/localization/locales/hi.json +156 -141
  215. package/shared/localization/locales/hr.json +154 -139
  216. package/shared/localization/locales/hu.json +111 -96
  217. package/shared/localization/locales/id.json +153 -138
  218. package/shared/localization/locales/it.json +153 -138
  219. package/shared/localization/locales/ja.json +150 -135
  220. package/shared/localization/locales/ko.json +42 -27
  221. package/shared/localization/locales/lt.json +152 -137
  222. package/shared/localization/locales/lv.json +93 -78
  223. package/shared/localization/locales/nl.json +149 -134
  224. package/shared/localization/locales/no.json +152 -137
  225. package/shared/localization/locales/pl.json +153 -138
  226. package/shared/localization/locales/pt-PT.json +152 -137
  227. package/shared/localization/locales/pt.json +157 -142
  228. package/shared/localization/locales/ro.json +149 -134
  229. package/shared/localization/locales/ru.json +76 -61
  230. package/shared/localization/locales/sk.json +153 -138
  231. package/shared/localization/locales/sl.json +149 -134
  232. package/shared/localization/locales/sr-Latn.json +151 -136
  233. package/shared/localization/locales/sr.json +151 -136
  234. package/shared/localization/locales/sv.json +152 -137
  235. package/shared/localization/locales/ta.json +156 -141
  236. package/shared/localization/locales/te.json +157 -142
  237. package/shared/localization/locales/th.json +111 -96
  238. package/shared/localization/locales/tr.json +153 -138
  239. package/shared/localization/locales/uk.json +149 -134
  240. package/shared/localization/locales/vi.json +162 -147
  241. package/shared/localization/locales/zh-HK.json +150 -135
  242. package/shared/localization/locales/zh-TW.json +154 -139
  243. package/shared/localization/locales/zh.json +152 -137
  244. package/shared/localization/swap-locale.js +10 -5
  245. package/shared/root.js +1 -3
  246. package/third-party/ard/README.md +19 -10
  247. package/third-party/ard/ard-test.js +189 -266
  248. package/third-party/ard/ard.d.ts +18 -1
  249. package/third-party/ard/ard.js +282 -45
  250. package/third-party/ard/schema-validator.d.ts +14 -5
  251. package/third-party/ard/schema-validator.js +921 -1177
  252. package/third-party/ard/spec/schemas/ard-entry.schema.json +176 -0
  253. package/tsconfig.json +0 -2
  254. package/types/artifacts.d.ts +43 -7
  255. package/types/externs.d.ts +4 -0
  256. package/types/internal/test.d.ts +1 -0
  257. package/build-tracker.config.js +0 -16
  258. package/core/lib/cdt/package.json +0 -4
  259. package/eslint-local-rules.d.cts +0 -9
  260. package/eslint.config.mjs +0 -242
  261. package/third-party/ard/spec/schemas/ai-catalog.schema.json +0 -292
  262. package/third-party/esbuild-plugins-polyfills/LICENSE +0 -3
  263. package/third-party/esbuild-plugins-polyfills/README.md +0 -7
  264. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.d.ts +0 -9
  265. package/third-party/esbuild-plugins-polyfills/esbuild-polyfills.js +0 -129
  266. package/types/internal/lookup-closest-locale.d.ts +0 -13
@@ -18,7 +18,7 @@ const UIStrings = {
18
18
  */
19
19
  description:
20
20
  'Lists web features used on the page and their Baseline status as of {date}. ' +
21
- '[Learn more about Baseline](https://webstatus.dev/).',
21
+ '[Learn more about Baseline](https://developer.chrome.com/docs/lighthouse/best-practices/baseline-features).',
22
22
  /** Label for the column displaying the feature ID. */
23
23
  columnFeature: 'Web-features',
24
24
  /** Label for the column displaying the feature\'s baseline status. */
@@ -48,6 +48,18 @@ class Baseline extends Audit {
48
48
 
49
49
  /**
50
50
  * Determines the baseline status and display string for a given feature ID.
51
+ *
52
+ * Baseline tiers & dates (built by `build/build-baseline-data.js`):
53
+ * - `limited`: `baseline === false` (Limited Availability — not yet in all core browsers, no date).
54
+ * - `low`: `baseline === 'low'` (Newly Available — in all core browsers; value is `baseline_low_date`).
55
+ * - `high`: `baseline === 'high'` (Widely Available — 30 months past `baseline_low_date`; note that
56
+ * `featureData.high[id]` still stores the feature's `baseline_low_date`).
57
+ *
58
+ * Widely Available rows always show `baseline_low_date`, including rows promoted at runtime.
59
+ * If a feature in `featureData.low` has reached `baseline_low_date + 30 months <= currentDate` at runtime,
60
+ * we dynamically promote it from `low` (Newly Available) to `high` (Widely Available); promotion
61
+ * only changes the tier.
62
+ *
51
63
  * @param {string} featureId
52
64
  * @param {{high: Record<string, string>, low: Record<string, string>, limited: string[]}} featureData
53
65
  * @param {Date} currentDate
@@ -66,11 +78,10 @@ class Baseline extends Audit {
66
78
  const lowData = /** @type {Record<string, string>} */ (featureData.low);
67
79
  const widelyAvailableDate = new Date(lowData[featureId]);
68
80
  widelyAvailableDate.setUTCMonth(widelyAvailableDate.getUTCMonth() + 30);
69
- const widelyAvailableDateStr = widelyAvailableDate.toISOString().slice(0, 10);
70
81
 
71
82
  if (widelyAvailableDate <= currentDate) {
72
83
  return {
73
- displayStatus: `Widely Available (${widelyAvailableDateStr})`,
84
+ displayStatus: `Widely Available (${lowData[featureId]})`,
74
85
  baselineTier: 'high',
75
86
  };
76
87
  } else {
@@ -92,6 +103,10 @@ class Baseline extends Audit {
92
103
  }
93
104
 
94
105
  /**
106
+ * Returns the `baseline_low_date` (YYYY-MM-DD, when the feature became Newly Available)
107
+ * for both `low` and `high` features, since `build/build-baseline-data.js` stores
108
+ * `baseline_low_date` in both `featureData.low` and `featureData.high`.
109
+ *
95
110
  * @param {string} featureId
96
111
  * @param {{high: Record<string, string>, low: Record<string, string>, limited: string[]}} featureData
97
112
  * @return {string|null}
@@ -103,9 +118,7 @@ class Baseline extends Audit {
103
118
  }
104
119
  if (featureId in featureData.high) {
105
120
  const highData = /** @type {Record<string, string>} */ (featureData.high);
106
- const date = new Date(highData[featureId]);
107
- date.setUTCMonth(date.getUTCMonth() - 30);
108
- return date.toISOString().slice(0, 10);
121
+ return highData[featureId];
109
122
  }
110
123
  return null;
111
124
  }
@@ -10,7 +10,7 @@ import {NetworkRecords} from '../../computed/network-records.js';
10
10
  import {LoadSimulator} from '../../computed/load-simulator.js';
11
11
  import {LanternLargestContentfulPaint as LanternLCP} from '../../computed/metrics/lantern-largest-contentful-paint.js';
12
12
  import {LanternFirstContentfulPaint as LanternFCP} from '../../computed/metrics/lantern-first-contentful-paint.js';
13
- import {LCPImageRecord} from '../../computed/lcp-image-record.js';
13
+ import {LCPNetworkRecord} from '../../computed/lcp-network-record.js';
14
14
 
15
15
  const str_ = i18n.createIcuMessageFn(import.meta.url, {});
16
16
 
@@ -190,7 +190,7 @@ class ByteEfficiencyAudit extends Audit {
190
190
 
191
191
  // The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
192
192
  let lcpRecordSavings = 0;
193
- const lcpRecord = await LCPImageRecord.request(metricComputationInput, context);
193
+ const lcpRecord = await LCPNetworkRecord.request(metricComputationInput, context);
194
194
  if (lcpRecord) {
195
195
  const lcpResult = results.find(result => result.url === lcpRecord.url);
196
196
  if (lcpResult) {
@@ -21,6 +21,15 @@ export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/tr
21
21
  numericValue?: number;
22
22
  numericUnit?: LH.Audit.NumericProduct["numericUnit"];
23
23
  } | LH.Audit.Details | undefined): Promise<LH.Audit.Product>;
24
+ /**
25
+ * @param {LH.Artifacts} artifacts
26
+ * @param {LH.Audit.Context} context
27
+ * @return {Promise<{insights: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightSet|undefined, data: LH.Artifacts.TraceEngineResult['data']}>}
28
+ */
29
+ export function getInsightSet(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<{
30
+ insights: import("@paulirish/trace_engine/models/trace/insights/types.js").InsightSet | undefined;
31
+ data: LH.Artifacts.TraceEngineResult["data"];
32
+ }>;
24
33
  /**
25
34
  * @param {LH.Artifacts.TraceElement[]} traceElements
26
35
  * @param {number|null|undefined} nodeId
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {NO_NAVIGATION} from '@paulirish/trace_engine/models/trace/types/TraceEvents.js';
8
8
 
9
+ import {Helpers} from '../../lib/trace-engine.js';
9
10
  import {ProcessedTrace} from '../../computed/processed-trace.js';
10
11
  import {TraceEngineResult} from '../../computed/trace-engine-result.js';
11
12
  import {Audit} from '../audit.js';
@@ -28,8 +29,8 @@ async function getInsightSet(artifacts, context) {
28
29
 
29
30
  const navigationId = processedTrace.timeOriginEvt.args.data?.navigationId;
30
31
  const insights = navigationId ?
31
- [...traceEngineResult.insights.values()]
32
- .find(insightSet => insightSet.navigation?.args.data?.navigationId) :
32
+ [...traceEngineResult.insights.values()].find(insightSet =>
33
+ Helpers.Trace.navigationIDForEvent(insightSet.navigation) === navigationId) :
33
34
  traceEngineResult.insights.get(NO_NAVIGATION);
34
35
 
35
36
  return {insights, data: traceEngineResult.data};
@@ -136,36 +137,41 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
136
137
  // TODO: consider adding a `estimatedSavingsText` to InsightModel, which can capture
137
138
  // the exact i18n string used by RPP; and include the same est. timing savings.
138
139
  let displayValue;
140
+ let wastedMs;
141
+
142
+ switch (insight.insightKey) {
143
+ case 'DocumentLatency':
144
+ case 'FontDisplay':
145
+ case 'RenderBlocking': {
146
+ wastedMs = metricSavings?.FCP ?? 0;
147
+ break;
148
+ }
139
149
 
140
- if (insight.wastedBytes) {
141
- displayValue = str_(i18n.UIStrings.displayValueByteSavings, {wastedBytes: insight.wastedBytes});
142
- } else {
143
- let wastedMs;
144
-
145
- switch (insight.insightKey) {
146
- case 'DocumentLatency':
147
- case 'DuplicatedJavaScript':
148
- case 'FontDisplay':
149
- case 'LegacyJavaScript':
150
- case 'RenderBlocking': {
151
- wastedMs = metricSavings?.FCP;
152
- break;
153
- }
154
-
155
- case 'LCPDiscovery':
156
- case 'ModernHTTP': {
157
- wastedMs = metricSavings?.LCP;
158
- break;
159
- }
150
+ case 'LCPDiscovery':
151
+ case 'ModernHTTP': {
152
+ wastedMs = metricSavings?.LCP ?? 0;
153
+ break;
154
+ }
160
155
 
161
- case 'Viewport': {
162
- wastedMs = metricSavings?.INP;
163
- break;
164
- }
156
+ case 'Viewport': {
157
+ wastedMs = metricSavings?.INP ?? 0;
158
+ break;
165
159
  }
160
+ }
161
+
162
+ if (insight.wastedBytes) {
163
+ displayValue = str_(i18n.UIStrings.displayValueByteSavings, {wastedBytes: insight.wastedBytes});
164
+ } else if (wastedMs) {
165
+ displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs});
166
+ }
166
167
 
167
- if (wastedMs) {
168
- displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs});
168
+ if (numericValue === undefined) {
169
+ if (wastedMs !== undefined) {
170
+ numericValue = wastedMs;
171
+ numericUnit = 'millisecond';
172
+ } else if (insight.wastedBytes !== undefined) {
173
+ numericValue = insight.wastedBytes;
174
+ numericUnit = 'byte';
169
175
  }
170
176
  }
171
177
 
@@ -225,5 +231,6 @@ function makeNodeItemForNodeId(traceElements, nodeId) {
225
231
 
226
232
  export {
227
233
  adaptInsightToAuditProduct,
234
+ getInsightSet,
228
235
  makeNodeItemForNodeId,
229
236
  };
@@ -192,9 +192,10 @@ class LongTasks extends Audit {
192
192
  taskTimingsByEvent = new Map();
193
193
 
194
194
  const simulatorOptions = {devtoolsLog, settings: context.settings};
195
+ const fromTrace = process.env.INTERNAL_LANTERN_USE_TRACE !== undefined;
195
196
  const pageGraph =
196
197
  // eslint-disable-next-line max-len
197
- await PageDependencyGraph.request({settings, trace, devtoolsLog, URL, SourceMaps, HostDPR, fromTrace: false}, context);
198
+ await PageDependencyGraph.request({settings, trace, devtoolsLog, URL, SourceMaps, HostDPR, fromTrace}, context);
198
199
  const simulator = await LoadSimulator.request(simulatorOptions, context);
199
200
  const simulation = simulator.simulate(pageGraph, {label: 'long-tasks-diagnostic'});
200
201
  for (const [node, timing] of simulation.nodeTimings.entries()) {
@@ -76,8 +76,8 @@ class PredictivePerf extends Audit {
76
76
  pessimisticLCP: lcp.pessimisticEstimate.timeInMs,
77
77
 
78
78
  roughEstimateOfTTFB: timingSummary.metrics.timeToFirstByte,
79
- roughEstimateOfLCPLoadStart: timingSummary.metrics.lcpLoadDelay,
80
- roughEstimateOfLCPLoadEnd: timingSummary.metrics.lcpLoadDuration,
79
+ roughEstimateOfLCPLoadDelay: timingSummary.metrics.lcpLoadDelay,
80
+ roughEstimateOfLCPLoadDuration: timingSummary.metrics.lcpLoadDuration,
81
81
  };
82
82
 
83
83
  const score = Audit.computeLogNormalScore(
@@ -2,19 +2,6 @@ export default ScreenshotThumbnails;
2
2
  export type SpeedlineFrame = LH.Artifacts.Speedline["frames"][0];
3
3
  /** @typedef {LH.Artifacts.Speedline['frames'][0]} SpeedlineFrame */
4
4
  declare class ScreenshotThumbnails extends Audit {
5
- /**
6
- * Scales down an image to THUMBNAIL_WIDTH using nearest neighbor for speed, maintains aspect
7
- * ratio of the original thumbnail.
8
- *
9
- * @param {ReturnType<SpeedlineFrame['getParsedImage']>} imageData
10
- * @param {number} scaledWidth
11
- * @return {{width: number, height: number, data: Uint8Array}}
12
- */
13
- static scaleImageToThumbnail(imageData: ReturnType<SpeedlineFrame["getParsedImage"]>, scaledWidth: number): {
14
- width: number;
15
- height: number;
16
- data: Uint8Array;
17
- };
18
5
  /**
19
6
  * @param {LH.Artifacts} artifacts
20
7
  * @param {LH.Audit.Context} context
@@ -4,8 +4,6 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import jpeg from 'jpeg-js';
8
-
9
7
  import {Audit} from './audit.js';
10
8
  import {LighthouseError} from '../lib/lh-error.js';
11
9
  import {Speedline} from '../computed/speedline.js';
@@ -28,42 +26,6 @@ class ScreenshotThumbnails extends Audit {
28
26
  };
29
27
  }
30
28
 
31
- /**
32
- * Scales down an image to THUMBNAIL_WIDTH using nearest neighbor for speed, maintains aspect
33
- * ratio of the original thumbnail.
34
- *
35
- * @param {ReturnType<SpeedlineFrame['getParsedImage']>} imageData
36
- * @param {number} scaledWidth
37
- * @return {{width: number, height: number, data: Uint8Array}}
38
- */
39
- static scaleImageToThumbnail(imageData, scaledWidth) {
40
- const scaleFactor = imageData.width / scaledWidth;
41
- const scaledHeight = Math.floor(imageData.height / scaleFactor);
42
-
43
- const outPixels = new Uint8Array(scaledWidth * scaledHeight * 4);
44
-
45
- for (let i = 0; i < scaledWidth; i++) {
46
- for (let j = 0; j < scaledHeight; j++) {
47
- const origX = Math.floor(i * scaleFactor);
48
- const origY = Math.floor(j * scaleFactor);
49
-
50
- const origPos = (origY * imageData.width + origX) * 4;
51
- const outPos = (j * scaledWidth + i) * 4;
52
-
53
- outPixels[outPos] = imageData.data[origPos];
54
- outPixels[outPos + 1] = imageData.data[origPos + 1];
55
- outPixels[outPos + 2] = imageData.data[origPos + 2];
56
- outPixels[outPos + 3] = imageData.data[origPos + 3];
57
- }
58
- }
59
-
60
- return {
61
- width: scaledWidth,
62
- height: scaledHeight,
63
- data: outPixels,
64
- };
65
- }
66
-
67
29
  /**
68
30
  * @param {LH.Artifacts} artifacts
69
31
  * @param {LH.Audit.Context} context
@@ -79,7 +41,6 @@ class ScreenshotThumbnails extends Audit {
79
41
  // Make the minimum time range 3s so sites that load super quickly don't get a single screenshot
80
42
  const minimumTimelineDuration = context.options.minimumTimelineDuration || 3000;
81
43
  const numberOfThumbnails = context.options.numberOfThumbnails || NUMBER_OF_THUMBNAILS;
82
- const thumbnailWidth = context.options.thumbnailWidth || null;
83
44
 
84
45
  const thumbnails = [];
85
46
  const analyzedFrames = speedline.frames.filter(frame => !frame.isProgressInterpolated());
@@ -112,12 +73,6 @@ class ScreenshotThumbnails extends Audit {
112
73
  const cachedThumbnail = cachedThumbnails.get(frameForTimestamp);
113
74
  if (cachedThumbnail) {
114
75
  base64Data = cachedThumbnail;
115
- } else if (thumbnailWidth !== null) {
116
- const imageData = frameForTimestamp.getParsedImage();
117
- const thumbnailImageData =
118
- ScreenshotThumbnails.scaleImageToThumbnail(imageData, thumbnailWidth);
119
- base64Data = jpeg.encode(thumbnailImageData, 90).data.toString('base64');
120
- cachedThumbnails.set(frameForTimestamp, base64Data);
121
76
  } else {
122
77
  base64Data = frameForTimestamp.getImage().toString('base64');
123
78
  cachedThumbnails.set(frameForTimestamp, base64Data);
@@ -176,11 +176,32 @@ class ScriptTreemapDataAudit extends Audit {
176
176
  const nodes = [];
177
177
  /** @type {Map<string, LH.Treemap.Node>} */
178
178
  const htmlNodesByFrameId = new Map();
179
- const bundles = await JSBundles.request(artifacts, context);
180
- const duplicationByPath = await ModuleDuplication.request(artifacts, context);
179
+ /** @type {Set<LH.Artifacts.NetworkRequest>} */
180
+ const seenNetworkRecords = new Set();
181
+ const deduplicatedScripts = artifacts.Scripts.filter(script => {
182
+ if (script.scriptLanguage !== 'JavaScript') return false;
183
+
184
+ const networkRecord = isInline(script) ?
185
+ undefined :
186
+ getRequestForScript(networkRecords, script);
187
+ if (networkRecord) {
188
+ if (seenNetworkRecords.has(networkRecord)) return false;
189
+ seenNetworkRecords.add(networkRecord);
190
+ }
191
+
192
+ return true;
193
+ });
194
+ const deduplicatedArtifacts = deduplicatedScripts.length === artifacts.Scripts.length ?
195
+ artifacts :
196
+ {...artifacts, Scripts: deduplicatedScripts};
197
+ const bundles = await JSBundles.request(deduplicatedArtifacts, context);
198
+ const duplicationByPath = await ModuleDuplication.request(deduplicatedArtifacts, context);
181
199
 
182
- for (const script of artifacts.Scripts) {
183
- if (script.scriptLanguage !== 'JavaScript') continue;
200
+ for (const script of deduplicatedScripts) {
201
+ const isInlineScript = isInline(script);
202
+ const networkRecord = isInlineScript ?
203
+ undefined :
204
+ getRequestForScript(networkRecords, script);
184
205
 
185
206
  const name = script.url;
186
207
  const bundle = bundles.find(bundle => script.scriptId === bundle.script.scriptId) ?? null;
@@ -246,7 +267,7 @@ class ScriptTreemapDataAudit extends Audit {
246
267
 
247
268
  // If this is an inline script, place the node inside a top-level (aka depth-one) node.
248
269
  // Also separate each iframe / the main page's inline scripts into their own top-level nodes.
249
- if (isInline(script)) {
270
+ if (isInlineScript) {
250
271
  let htmlNode = htmlNodesByFrameId.get(script.executionContextAuxData.frameId);
251
272
  if (!htmlNode) {
252
273
  htmlNode = {
@@ -269,7 +290,6 @@ class ScriptTreemapDataAudit extends Audit {
269
290
  // Non-inline scripts each have their own top-level node.
270
291
  nodes.push(node);
271
292
 
272
- const networkRecord = getRequestForScript(networkRecords, script);
273
293
  if (networkRecord) {
274
294
  const bodyTransferSize =
275
295
  networkRecord.transferSize - networkRecord.responseHeadersTransferSize;
@@ -27,7 +27,7 @@ const DIRECTIVE_SAFELIST = new Set([
27
27
  DIRECTIVE_ALLOW, DIRECTIVE_SITEMAP, // universally supported
28
28
  'crawl-delay', // yahoo, bing, yandex
29
29
  'clean-param', 'host', // yandex
30
- 'request-rate', 'visit-time', 'noindex', 'content-signal', // not officially supported, but used in the wild
30
+ 'request-rate', 'visit-time', 'noindex', 'content-signal', 'license', // not officially supported, but used in the wild
31
31
  ]);
32
32
  const SITEMAP_VALID_PROTOCOLS = new Set(['https:', 'http:', 'ftp:']);
33
33
 
@@ -209,7 +209,7 @@ class RobotsTxt extends Audit {
209
209
  } = artifacts.RobotsTxt;
210
210
 
211
211
  // Do specific error messages first.
212
- if (status && status >= HTTP_SERVER_ERROR_CODE_LOW) {
212
+ if (status && (status >= HTTP_SERVER_ERROR_CODE_LOW || status === 429)) {
213
213
  return {
214
214
  score: 0,
215
215
  displayValue: str_(UIStrings.displayValueHttpBadCode, {statusCode: status}),
@@ -228,18 +228,13 @@ class RobotsTxt extends Audit {
228
228
  };
229
229
  }
230
230
 
231
- if (!status) {
231
+ if (!status || content === null) {
232
232
  return {
233
233
  score: 0,
234
234
  explanation: str_(UIStrings.explanation),
235
235
  };
236
236
  }
237
237
 
238
- // If status is good, content must be not null.
239
- if (content === null) {
240
- throw new Error(`Status ${status} was valid, but content was null`);
241
- }
242
-
243
238
  const validationErrors = validateRobots(content);
244
239
 
245
240
  /** @type {LH.Audit.Details.Table['headings']} */
@@ -1,12 +1,5 @@
1
1
  export default UnsizedImages;
2
2
  declare class UnsizedImages extends Audit {
3
- /**
4
- * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
5
- * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
6
- * @param {string | null} attrValue
7
- * @return {boolean}
8
- */
9
- static doesHtmlAttrProvideExplicitSize(attrValue: string | null): boolean;
10
3
  /**
11
4
  * An img css size property prevents layout shifts if it is defined, not empty, and not equal to 'auto'.
12
5
  * @param {string | null} property
@@ -10,6 +10,7 @@
10
10
 
11
11
 
12
12
  import {Audit} from './audit.js';
13
+ import ImageElements from '../gather/gatherers/image-elements.js';
13
14
  import * as i18n from './../lib/i18n/i18n.js';
14
15
  import UrlUtils from '../lib/url-utils.js';
15
16
 
@@ -40,28 +41,6 @@ class UnsizedImages extends Audit {
40
41
  };
41
42
  }
42
43
 
43
- /**
44
- * An img size attribute prevents layout shifts if it is a non-negative integer (incl zero!).
45
- * @url https://html.spec.whatwg.org/multipage/embedded-content-other.html#dimension-attributes
46
- * @param {string | null} attrValue
47
- * @return {boolean}
48
- */
49
- static doesHtmlAttrProvideExplicitSize(attrValue) {
50
- if (!attrValue) return false;
51
-
52
- // First, superweird edge case of using the positive-sign. The spec _sorta_ says it's valid...
53
- // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#rules-for-parsing-integers
54
- // > Otherwise, if the character is … (+): Advance position to the next character.
55
- // > (The "+" is ignored, but it is not conforming.)
56
- // lol. Irrelevant though b/c Chrome (at least) doesn't ignore. It rejects this as a non-conforming value.
57
- if (attrValue.startsWith('+')) return false;
58
-
59
- // parseInt isn't exactly the same as the html's spec for parsing integers, but it's close enough
60
- // https://tc39.es/ecma262/#sec-parseint-string-radix
61
- const int = parseInt(attrValue, 10);
62
- return int >= 0;
63
- }
64
-
65
44
  /**
66
45
  * An img css size property prevents layout shifts if it is defined, not empty, and not equal to 'auto'.
67
46
  * @param {string | null} property
@@ -78,6 +57,12 @@ class UnsizedImages extends Audit {
78
57
  * @return {boolean}
79
58
  */
80
59
  static isSizedImage(image) {
60
+ const attrWidth = image.attributeWidth;
61
+ const attrHeight = image.attributeHeight;
62
+ const htmlWidthIsExplicit = ImageElements.doesHtmlAttrProvideExplicitSize(attrWidth);
63
+ const htmlHeightIsExplicit = ImageElements.doesHtmlAttrProvideExplicitSize(attrHeight);
64
+ if (htmlWidthIsExplicit && htmlHeightIsExplicit) return true;
65
+
81
66
  // Perhaps we hit reachedGatheringBudget before collecting this image's cssWidth/Height
82
67
  // in fetchSourceRules. In this case, we don't have enough information to determine if it's sized.
83
68
  // We don't want to show the user a false positive, so we'll call it sized to give it as pass.
@@ -85,14 +70,10 @@ class UnsizedImages extends Audit {
85
70
  // Handwavey TODO: explore ways to avoid this.
86
71
  if (image.cssEffectiveRules === undefined) return true;
87
72
 
88
- const attrWidth = image.attributeWidth;
89
- const attrHeight = image.attributeHeight;
90
73
  const cssWidth = image.cssEffectiveRules.width;
91
74
  const cssHeight = image.cssEffectiveRules.height;
92
75
  const cssAspectRatio = image.cssEffectiveRules.aspectRatio;
93
- const htmlWidthIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrWidth);
94
76
  const cssWidthIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssWidth);
95
- const htmlHeightIsExplicit = UnsizedImages.doesHtmlAttrProvideExplicitSize(attrHeight);
96
77
  const cssHeightIsExplicit = UnsizedImages.isCssPropExplicitlySet(cssHeight);
97
78
  const explicitAspectRatio = UnsizedImages.isCssPropExplicitlySet(cssAspectRatio);
98
79
  const explicitWidth = htmlWidthIsExplicit || cssWidthIsExplicit;
@@ -1,24 +1,3 @@
1
- export { LCPImageRecordComputed as LCPImageRecord };
2
- declare const LCPImageRecordComputed: typeof LCPImageRecord & {
3
- request: (dependencies: {
4
- trace: LH.Trace;
5
- devtoolsLog: LH.DevtoolsLog;
6
- }, context: LH.Artifacts.ComputedContext) => Promise<import("../lib/network-request.js").NetworkRequest | undefined>;
7
- };
8
- /**
9
- * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
10
- * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
11
- * was a pseudo-element associated with a node containing a smaller background-image.
12
- */
13
- declare class LCPImageRecord {
14
- /**
15
- * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
16
- * @param {LH.Artifacts.ComputedContext} context
17
- * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
18
- */
19
- static compute_(data: {
20
- trace: LH.Trace;
21
- devtoolsLog: LH.DevtoolsLog;
22
- }, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest | undefined>;
23
- }
1
+ export { LCPNetworkRecord as LCPImageRecord };
2
+ import { LCPNetworkRecord } from './lcp-network-record.js';
24
3
  //# sourceMappingURL=lcp-image-record.d.ts.map
@@ -4,71 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import {makeComputedArtifact} from './computed-artifact.js';
8
- import {NetworkRecords} from './network-records.js';
9
- import {ProcessedNavigation} from './processed-navigation.js';
10
- import {LighthouseError} from '../lib/lh-error.js';
11
-
12
- /**
13
- * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
14
- * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
15
- * was a pseudo-element associated with a node containing a smaller background-image.
16
- */
17
-
18
- class LCPImageRecord {
19
- /**
20
- * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
21
- * @param {LH.Artifacts.ComputedContext} context
22
- * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
23
- */
24
- static async compute_(data, context) {
25
- const {trace, devtoolsLog} = data;
26
- const networkRecords = await NetworkRecords.request(devtoolsLog, context);
27
- const processedNavigation = await ProcessedNavigation.request(trace, context);
28
- if (processedNavigation.timings.largestContentfulPaint === undefined) {
29
- throw new LighthouseError(LighthouseError.errors.NO_LCP);
30
- }
31
-
32
- // Use main-frame-only LCP to match the metric value.
33
- const lcpEvent = processedNavigation.largestContentfulPaintEvt;
34
- if (!lcpEvent) return;
35
-
36
- const lcpImagePaintEvent = trace.traceEvents.filter(e => {
37
- return e.name === 'LargestImagePaint::Candidate' &&
38
- e.args.frame === lcpEvent.args.frame &&
39
- e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
40
- e.args.data?.size === lcpEvent.args.data?.size;
41
- // Get last candidate, in case there was more than one.
42
- }).sort((a, b) => b.ts - a.ts)[0];
43
-
44
- const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
45
- if (!lcpUrl) return;
46
-
47
- const candidates = networkRecords.filter(record => {
48
- return record.url === lcpUrl &&
49
- record.finished &&
50
- // Same frame as LCP trace event.
51
- record.frameId === lcpImagePaintEvent.args.frame &&
52
- record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
53
- }).map(record => {
54
- // Follow any redirects to find the real image request.
55
- while (record.redirectDestination) {
56
- record = record.redirectDestination;
57
- }
58
- return record;
59
- }).filter(record => {
60
- // Don't select if also loaded by some other means (xhr, etc). `resourceType`
61
- // isn't set on redirect _sources_, so have to check after following redirects.
62
- return record.resourceType === 'Image';
63
- });
64
-
65
- // If there are still multiple candidates, at this point it appears the page
66
- // simply made multiple requests for the image. The first loaded is the best
67
- // guess of the request that made the image available for use.
68
- return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
69
- }
70
- }
71
-
72
- const LCPImageRecordComputed = makeComputedArtifact(LCPImageRecord, ['devtoolsLog', 'trace']);
73
- export {LCPImageRecordComputed as LCPImageRecord};
7
+ // TODO(v14): remove
8
+ import {LCPNetworkRecord} from './lcp-network-record.js';
74
9
 
10
+ export {LCPNetworkRecord as LCPImageRecord};
@@ -0,0 +1,24 @@
1
+ export { LCPNetworkRecordComputed as LCPNetworkRecord };
2
+ declare const LCPNetworkRecordComputed: typeof LCPNetworkRecord & {
3
+ request: (dependencies: {
4
+ trace: LH.Trace;
5
+ devtoolsLog: LH.DevtoolsLog;
6
+ }, context: LH.Artifacts.ComputedContext) => Promise<import("../lib/network-request.js").NetworkRequest | undefined>;
7
+ };
8
+ /**
9
+ * @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
10
+ * This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
11
+ * was a pseudo-element associated with a node containing a smaller background-image.
12
+ */
13
+ declare class LCPNetworkRecord {
14
+ /**
15
+ * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
16
+ * @param {LH.Artifacts.ComputedContext} context
17
+ * @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
18
+ */
19
+ static compute_(data: {
20
+ trace: LH.Trace;
21
+ devtoolsLog: LH.DevtoolsLog;
22
+ }, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest | undefined>;
23
+ }
24
+ //# sourceMappingURL=lcp-network-record.d.ts.map