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
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2023 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
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 LCPNetworkRecord {
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' || record.resourceType === 'Media';
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 LCPNetworkRecordComputed = makeComputedArtifact(LCPNetworkRecord, ['devtoolsLog', 'trace']);
73
+ export {LCPNetworkRecordComputed as LCPNetworkRecord};
74
+
@@ -40,7 +40,7 @@ async function getComputationDataParamsFromTrace(data, context) {
40
40
  const traceEngineResult = await TraceEngineResult.request(data, context);
41
41
  const frameId = traceEngineResult.data.Meta.mainFrameId;
42
42
  const navigation =
43
- traceEngineResult.data.Meta.mainFrameNavigations[0];
43
+ traceEngineResult.data.Meta.mainFrameNavigations.at(-1);
44
44
  if (!navigation) {
45
45
  throw new Error(`Lantern metrics could not be calculated due to missing navigation id`);
46
46
  }
@@ -9,7 +9,7 @@ import {LighthouseError} from '../../lib/lh-error.js';
9
9
  import {LargestContentfulPaint} from './largest-contentful-paint.js';
10
10
  import {ProcessedNavigation} from '../processed-navigation.js';
11
11
  import {TimeToFirstByte} from './time-to-first-byte.js';
12
- import {LCPImageRecord} from '../lcp-image-record.js';
12
+ import {LCPNetworkRecord} from '../lcp-network-record.js';
13
13
  import {NavigationInsights} from '../navigation-insights.js';
14
14
 
15
15
  /**
@@ -32,7 +32,7 @@ class LCPBreakdown {
32
32
 
33
33
  const {timing: ttfb} = await TimeToFirstByte.request(data, context);
34
34
 
35
- const lcpRecord = await LCPImageRecord.request(data, context);
35
+ const lcpRecord = await LCPNetworkRecord.request(data, context);
36
36
  if (!lcpRecord) {
37
37
  return {ttfb};
38
38
  }
@@ -42,15 +42,15 @@ class LCPBreakdown {
42
42
  const throttleRatio = metricLcp / observedLcp;
43
43
 
44
44
  const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
45
- const loadDelay = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
45
+ const loadStart = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
46
46
 
47
47
  const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
48
- const loadDuration = Math.max(loadDelay, Math.min(unclampedLoadEnd, metricLcp));
48
+ const loadEnd = Math.max(loadStart, Math.min(unclampedLoadEnd, metricLcp));
49
49
 
50
50
  return {
51
51
  ttfb,
52
- loadDelay,
53
- loadDuration,
52
+ loadDelay: loadStart - ttfb,
53
+ loadDuration: loadEnd - loadStart,
54
54
  };
55
55
  }
56
56
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {calculateDocFirstByteTs} from '@paulirish/trace_engine/models/trace/insights/Common.js';
8
8
 
9
+ import {Helpers} from '../../lib/trace-engine.js';
9
10
  import {makeComputedArtifact} from '../computed-artifact.js';
10
11
  import {NavigationMetric} from './navigation-metric.js';
11
12
  import {MainResource} from '../main-resource.js';
@@ -52,15 +53,16 @@ class TimeToFirstByte extends NavigationMetric {
52
53
  await NavigationInsights.request({trace, settings, SourceMaps, HostDPR}, context);
53
54
  const lcpBreakdown = navInsights.model.LCPBreakdown;
54
55
 
56
+ const navigationId = Helpers.Trace.navigationIDForEvent(navInsights.navigation);
57
+
55
58
  // Defer to LCP breakdown, but if there's no LCP fallback to manual calculation.
56
59
  if (lcpBreakdown && !(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
57
60
  return {
58
61
  timing: lcpBreakdown.subparts.ttfb.range / 1000,
59
62
  timestamp: lcpBreakdown.subparts.ttfb.max,
60
63
  };
61
- } else if (navInsights.navigation?.args.data?.navigationId) {
62
- const request = traceEngineResult.data.NetworkRequests.byId.get(
63
- navInsights.navigation.args.data.navigationId);
64
+ } else if (navInsights.navigation && navigationId) {
65
+ const request = traceEngineResult.data.NetworkRequests.byId.get(navigationId);
64
66
  if (!request) {
65
67
  throw new Error();
66
68
  }
@@ -4,6 +4,7 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import {Helpers} from '../lib/trace-engine.js';
7
8
  import {makeComputedArtifact} from './computed-artifact.js';
8
9
  import {ProcessedTrace} from './processed-trace.js';
9
10
  import {TraceEngineResult} from './trace-engine-result.js';
@@ -26,8 +27,8 @@ class NavigationInsights {
26
27
  const navigationId = processedTrace.timeOriginEvt.args.data?.navigationId;
27
28
  if (!navigationId) throw new Error('No navigationId found');
28
29
 
29
- const navInsights = [...traceEngineResult.insights.values()]
30
- .find(insightSet => insightSet.navigation?.args.data?.navigationId === navigationId);
30
+ const navInsights = [...traceEngineResult.insights.values()].find(insightSet =>
31
+ Helpers.Trace.navigationIDForEvent(insightSet.navigation) === navigationId);
31
32
  if (!navInsights) throw new Error('No navigations insights found');
32
33
 
33
34
  return navInsights;
@@ -11,6 +11,26 @@ import {ProcessedTrace} from './processed-trace.js';
11
11
  import {NetworkRecords} from './network-records.js';
12
12
  import {TraceEngineResult} from './trace-engine-result.js';
13
13
 
14
+ /**
15
+ * ResourceSendRequest URLs in the trace retain #hash fragments, whereas CDP
16
+ * Network.requestWillBeSent, Page.frameNavigated, initiator.url, documentURL,
17
+ * and V8 script events strip #hash fragments, and Lantern's NetworkAnalyzer
18
+ * requires request.url not to have a fragment.
19
+ * TODO: Remove once @paulirish/trace_engine includes the fix in LanternComputationData.
20
+ * @param {string} urlStr
21
+ * @return {string}
22
+ */
23
+ function stripUrlHash(urlStr) {
24
+ if (!urlStr.includes('#')) return urlStr;
25
+ try {
26
+ const url = new URL(urlStr);
27
+ url.hash = '';
28
+ return url.href;
29
+ } catch {
30
+ return urlStr;
31
+ }
32
+ }
33
+
14
34
  class PageDependencyGraph {
15
35
  /**
16
36
  * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, settings: LH.Audit.Context['settings'], URL: LH.Artifacts['URL'], SourceMaps: LH.Artifacts['SourceMaps'], HostDPR: LH.Artifacts['HostDPR'], fromTrace: boolean}} data
@@ -27,7 +47,41 @@ class PageDependencyGraph {
27
47
  if (data.fromTrace) {
28
48
  const traceEngineResult =
29
49
  await TraceEngineResult.request({trace, settings, SourceMaps, HostDPR}, context);
30
- const parsedTrace = traceEngineResult.data;
50
+ let parsedTrace = traceEngineResult.data;
51
+ if (parsedTrace.NetworkRequests.byTime.some(
52
+ r => r.args.data.url.includes('#') ||
53
+ r.args.data.redirects.some(red => red.url.includes('#'))
54
+ )) {
55
+ const byTime = parsedTrace.NetworkRequests.byTime.map(request => {
56
+ const url = stripUrlHash(request.args.data.url);
57
+ const redirects = request.args.data.redirects.map(redirect => ({
58
+ ...redirect,
59
+ url: stripUrlHash(redirect.url),
60
+ }));
61
+ if (url === request.args.data.url &&
62
+ redirects.every((red, i) => red.url === request.args.data.redirects[i].url)) {
63
+ return request;
64
+ }
65
+ return {
66
+ ...request,
67
+ args: {
68
+ ...request.args,
69
+ data: {
70
+ ...request.args.data,
71
+ url,
72
+ redirects,
73
+ },
74
+ },
75
+ };
76
+ });
77
+ parsedTrace = {
78
+ ...parsedTrace,
79
+ NetworkRequests: {
80
+ ...parsedTrace.NetworkRequests,
81
+ byTime,
82
+ },
83
+ };
84
+ }
31
85
  const requests =
32
86
  Lantern.TraceEngineComputationData.createNetworkRequests(trace, parsedTrace);
33
87
  const graph =
@@ -36,10 +90,6 @@ class PageDependencyGraph {
36
90
  return graph;
37
91
  }
38
92
 
39
- // TODO: currently the trace version has no requests that failed, or requests that have "Preflight".
40
- // so the following gets the devtools log version _closer_ to the exact same results as the trace.
41
- // const lanternRequests = networkRecords.map(NetworkRequest.asLanternNetworkRequest).filter(r => !r.failed && r.resourceType !== 'Preflight');
42
-
43
93
  const lanternRequests = networkRecords.map(NetworkRequest.asLanternNetworkRequest);
44
94
  return Lantern.Graph.PageDependencyGraph.createGraph(mainThreadEvents, lanternRequests, URL);
45
95
  }
@@ -20,13 +20,10 @@ class Speedline {
20
20
  // speedline() may throw without a promise, so we resolve immediately
21
21
  // to get in a promise chain.
22
22
  return ProcessedTrace.request(trace, context).then(processedTrace => {
23
- // Use a shallow copy of traceEvents so speedline can sort as it pleases.
24
- // See https://github.com/GoogleChrome/lighthouse/issues/2333
25
- const traceEvents = trace.traceEvents.slice();
26
23
  // Force use of timeOrigin as reference point for speedline
27
24
  // See https://github.com/GoogleChrome/lighthouse/issues/2095
28
25
  const timeOrigin = processedTrace.timestamps.timeOrigin;
29
- return speedline(traceEvents, {
26
+ return speedline(trace.traceEvents, {
30
27
  timeOrigin,
31
28
  fastMode: true,
32
29
  include: 'speedIndex',
@@ -46,7 +43,25 @@ class Speedline {
46
43
  throw new LighthouseError(LighthouseError.errors.SPEEDINDEX_OF_ZERO);
47
44
  }
48
45
 
49
- return speedline;
46
+ // Evict the ~1 MB per-frame decoded RGBA buffers and histograms cached inside
47
+ // speedline-core's frame closures, keeping only the getters Lighthouse uses.
48
+ // TODO(https://github.com/paulirish/speedline/issues/122): Upstream to speedline-core
49
+ // so `frame()` in `lib/frame.js` does not cache `_parsedImage` on frame closures.
50
+ const frames = speedline.frames.map(frame => {
51
+ const imgBuff = frame.getImage();
52
+ const timeStamp = frame.getTimeStamp();
53
+ const isProgressInterpolated = frame.isProgressInterpolated();
54
+ return {
55
+ getImage: () => imgBuff,
56
+ getTimeStamp: () => timeStamp,
57
+ isProgressInterpolated: () => isProgressInterpolated,
58
+ };
59
+ });
60
+
61
+ return {
62
+ ...speedline,
63
+ frames,
64
+ };
50
65
  });
51
66
  }
52
67
  }
@@ -7,6 +7,7 @@
7
7
  import log from 'lighthouse-logger';
8
8
 
9
9
  import * as i18n from '../lib/i18n/i18n.js';
10
+ import * as Lantern from '../lib/lantern/lantern.js';
10
11
  import * as TraceEngine from '../lib/trace-engine.js';
11
12
  import {makeComputedArtifact} from './computed-artifact.js';
12
13
  import {CumulativeLayoutShift} from './metrics/cumulative-layout-shift.js';
@@ -26,7 +27,9 @@ class TraceEngineResult {
26
27
  * @return {Promise<LH.Artifacts.TraceEngineResult>}
27
28
  */
28
29
  static async runTraceEngine(_traceEvents, settings, SourceMaps, HostDPR) {
29
- const processor = new TraceEngine.TraceProcessor(TraceEngine.TraceHandlers);
30
+ const config = TraceEngine.Types.Configuration.defaults();
31
+ config.enableSoftNavigation = false;
32
+ const processor = new TraceEngine.TraceProcessor(TraceEngine.TraceHandlers, config);
30
33
  const traceEvents =
31
34
  /** @type {import('@paulirish/trace_engine').Types.Events.Event[]} */ (_traceEvents);
32
35
 
@@ -66,8 +69,20 @@ class TraceEngineResult {
66
69
  });
67
70
  if (!processor.data) throw new Error('No data');
68
71
  if (!processor.insights) throw new Error('No insights');
69
- this.localizeInsights(processor.insights);
70
- return {data: processor.data, insights: processor.insights};
72
+ const data = processor.data;
73
+ const insights = processor.insights;
74
+ this.localizeInsights(insights);
75
+
76
+ // Release intermediate handler state retained in module-level TraceHandlers singletons
77
+ // and static Lantern simulation node timings after trace processing completes.
78
+ for (const handler of Object.values(TraceEngine.TraceHandlers)) {
79
+ handler.reset();
80
+ }
81
+ if (!process.env.LANTERN_DEBUG) {
82
+ Lantern.Simulation.Simulator.allNodeTimings.clear();
83
+ }
84
+
85
+ return {data, insights};
71
86
  }
72
87
 
73
88
  /**
@@ -236,8 +251,9 @@ class TraceEngineResult {
236
251
  const layoutShiftEvents = new Set(
237
252
  CumulativeLayoutShift.getLayoutShiftEvents(processedTrace).map(e => e.event));
238
253
 
239
- // Avoid modifying the input array.
240
- const traceEvents = [...data.trace.traceEvents];
254
+ // Avoid modifying the input array, copying only if an event is modified.
255
+ let traceEvents = data.trace.traceEvents;
256
+ let copied = false;
241
257
  for (let i = 0; i < traceEvents.length; i++) {
242
258
  let event = traceEvents[i];
243
259
  if (event.name !== 'LayoutShift') continue;
@@ -245,6 +261,10 @@ class TraceEngineResult {
245
261
 
246
262
  const isConsidered = layoutShiftEvents.has(event);
247
263
  if (event.args.data.had_recent_input && isConsidered) {
264
+ if (!copied) {
265
+ traceEvents = [...traceEvents];
266
+ copied = true;
267
+ }
248
268
  event = JSON.parse(JSON.stringify(event));
249
269
  // @ts-expect-error impossible for data to be missing.
250
270
  event.args.data.had_recent_input = false;
@@ -66,8 +66,21 @@ class UnusedCSS {
66
66
  let usedUncompressedBytes = 0;
67
67
  const totalUncompressedBytes = stylesheetInfo.content.length;
68
68
 
69
- for (const usedRule of stylesheetInfo.usedRules) {
70
- usedUncompressedBytes += usedRule.endOffset - usedRule.startOffset;
69
+ // CSS rule usage tracking can return overlapping ranges (e.g. a nested rule inside its
70
+ // parent rule's range) or duplicate ranges (parent rules are emitted once per used child rule).
71
+ // Sort and merge overlapping ranges so used bytes are only counted once.
72
+ const usedRules = stylesheetInfo.usedRules
73
+ .slice()
74
+ .sort((a, b) => a.startOffset - b.startOffset);
75
+
76
+ let lastEndOffset = -1;
77
+ for (const usedRule of usedRules) {
78
+ if (usedRule.endOffset <= lastEndOffset) {
79
+ continue;
80
+ }
81
+ const startOffset = Math.max(usedRule.startOffset, lastEndOffset);
82
+ usedUncompressedBytes += usedRule.endOffset - startOffset;
83
+ lastEndOffset = usedRule.endOffset;
71
84
  }
72
85
 
73
86
  const compressedSize = estimateCompressedContentSize(
@@ -1,6 +1,8 @@
1
1
  export type GathererConstructor = typeof import("../gather/base-gatherer.js").default;
2
2
  export type Audit = typeof import("../audits/audit.js")["Audit"];
3
3
  export type Gatherer = InstanceType<GathererConstructor>;
4
+ /** @type {Map<string, Promise<any>>} */
5
+ export const bundledModules: Map<string, Promise<any>>;
4
6
  /**
5
7
  * // TODO(bckenny): could adopt "jsonified" type to ensure T will survive JSON
6
8
  * round trip: https://github.com/Microsoft/TypeScript/issues/21838
@@ -33,6 +35,9 @@ export const mergeConfigFragment: LH.Config.Merge;
33
35
  * @return {Array<T>}
34
36
  */
35
37
  export function mergeConfigFragmentArrayByKey<T extends Record<string, any>>(baseArray: Array<T> | null | undefined, extensionArray: Array<T> | null | undefined, keyFn: (item: T) => string): Array<T>;
38
+ /** @typedef {typeof import('../gather/base-gatherer.js').default} GathererConstructor */
39
+ /** @typedef {typeof import('../audits/audit.js')['Audit']} Audit */
40
+ /** @typedef {InstanceType<GathererConstructor>} Gatherer */
36
41
  /**
37
42
  * If any items with identical `path` properties are found in the input array,
38
43
  * merge their `options` properties into the first instance and then discard any
@@ -7,15 +7,13 @@ import path from 'path';
7
7
  import {createRequire} from 'module';
8
8
  import url from 'url';
9
9
 
10
- import {isEqual} from 'lodash-es';
11
-
12
10
  import * as LH from '../../types/lh.js';
13
11
  import * as constants from './constants.js';
14
12
  import ConfigPlugin from './config-plugin.js';
15
13
  import {Runner} from '../runner.js';
16
14
  import * as i18n from '../lib/i18n/i18n.js';
17
15
  import * as validation from './validation.js';
18
- import {getModuleDirectory} from '../../shared/esm-utils.js';
16
+ import {isEqual} from '../lib/arbitrary-equality-map.js';
19
17
 
20
18
  const require = createRequire(import.meta.url);
21
19
 
@@ -23,21 +21,6 @@ const require = createRequire(import.meta.url);
23
21
  /** @typedef {typeof import('../audits/audit.js')['Audit']} Audit */
24
22
  /** @typedef {InstanceType<GathererConstructor>} Gatherer */
25
23
 
26
- function isBundledEnvironment() {
27
- // If we're in DevTools or LightRider, we are definitely bundled.
28
- // TODO: refactor and delete `global.isDevtools`.
29
- if (global.isDevtools || global.isLightrider) return true;
30
-
31
- try {
32
- // Not foolproof, but `lighthouse-logger` is a dependency of lighthouse that should always be resolvable.
33
- // `require.resolve` will only throw in atypical/bundled environments.
34
- require.resolve('lighthouse-logger');
35
- return false;
36
- } catch (err) {
37
- return true;
38
- }
39
- }
40
-
41
24
  /**
42
25
  * If any items with identical `path` properties are found in the input array,
43
26
  * merge their `options` properties into the first instance and then discard any
@@ -226,6 +209,8 @@ async function requireWrapper(requirePath) {
226
209
  let module;
227
210
  if (bundledModules.has(requirePath)) {
228
211
  module = await bundledModules.get(requirePath);
212
+ } else if (bundledModules.size > 0) {
213
+ throw new Error(`Unable to locate module '${requirePath}' in bundled Lighthouse`);
229
214
  } else if (requirePath.match(/\.(js|mjs|cjs)$/)) {
230
215
  module = await import(requirePath);
231
216
  } else {
@@ -285,18 +270,12 @@ function requireAudit(auditPath, coreAuditList, configDir) {
285
270
  const coreAudit = coreAuditList.find(a => a === auditPathJs);
286
271
  let requirePath = `../audits/${auditPath}`;
287
272
  if (!coreAudit) {
288
- if (isBundledEnvironment()) {
273
+ if (bundledModules.has(auditPath)) {
289
274
  // This is for plugin bundling.
290
275
  requirePath = auditPath;
291
276
  } else {
292
277
  // Otherwise, attempt to find it elsewhere. This throws if not found.
293
- const absolutePath = resolveModulePath(auditPath, configDir, 'audit');
294
- if (isBundledEnvironment()) {
295
- // Use a relative path so bundler can easily expose it.
296
- requirePath = path.relative(getModuleDirectory(import.meta), absolutePath);
297
- } else {
298
- requirePath = absolutePath;
299
- }
278
+ requirePath = resolveModulePath(auditPath, configDir, 'audit');
300
279
  }
301
280
  }
302
281
 
@@ -374,7 +353,7 @@ async function mergePlugins(config, configDir, flags) {
374
353
  validation.assertValidPluginName(config, pluginName);
375
354
 
376
355
  // In bundled contexts, `resolveModulePath` will fail, so use the raw pluginName directly.
377
- const pluginPath = isBundledEnvironment() ?
356
+ const pluginPath = bundledModules.has(pluginName) ?
378
357
  pluginName :
379
358
  resolveModulePath(pluginName, configDir, 'plugin');
380
359
  const rawPluginJson = await requireWrapper(pluginPath);
@@ -513,7 +492,7 @@ function resolveModulePath(moduleIdentifier, configDir, category) {
513
492
  const errorString = 'Unable to locate ' + (category ? `${category}: ` : '') +
514
493
  `\`${moduleIdentifier}\`.
515
494
  Tried to resolve the module from these locations:
516
- ${getModuleDirectory(import.meta)}
495
+ ${import.meta.dirname}
517
496
  ${cwdPath}`;
518
497
 
519
498
  if (!configDir) {
@@ -606,6 +585,7 @@ function deepCloneConfigJson(json) {
606
585
  }
607
586
 
608
587
  export {
588
+ bundledModules,
609
589
  deepClone,
610
590
  deepCloneConfigJson,
611
591
  mergeConfigFragment,
@@ -28,11 +28,10 @@ import {
28
28
  mergeConfigFragment,
29
29
  mergeConfigFragmentArrayByKey,
30
30
  } from './config-helpers.js';
31
- import {getModuleDirectory} from '../../shared/esm-utils.js';
32
31
  import * as format from '../../shared/localization/format.js';
33
32
 
34
33
  const defaultConfigPath = path.join(
35
- getModuleDirectory(import.meta),
34
+ import.meta.dirname,
36
35
  '../../config/default-config.js'
37
36
  );
38
37
 
@@ -1,31 +1,4 @@
1
- export const throttling: {
2
- DEVTOOLS_RTT_ADJUSTMENT_FACTOR: number;
3
- DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR: number;
4
- mobileSlow4G: {
5
- rttMs: number;
6
- throughputKbps: number;
7
- requestLatencyMs: number;
8
- downloadThroughputKbps: number;
9
- uploadThroughputKbps: number;
10
- cpuSlowdownMultiplier: number;
11
- };
12
- mobileRegular3G: {
13
- rttMs: number;
14
- throughputKbps: number;
15
- requestLatencyMs: number;
16
- downloadThroughputKbps: number;
17
- uploadThroughputKbps: number;
18
- cpuSlowdownMultiplier: number;
19
- };
20
- desktopDense4G: {
21
- rttMs: number;
22
- throughputKbps: number;
23
- cpuSlowdownMultiplier: number;
24
- requestLatencyMs: number;
25
- downloadThroughputKbps: number;
26
- uploadThroughputKbps: number;
27
- };
28
- };
1
+ export const throttling: Lantern.Simulation.ThrottlingSettings;
29
2
  export namespace screenEmulationMetrics {
30
3
  export { MOTOGPOWER_EMULATION_METRICS as mobile };
31
4
  export { DESKTOP_EMULATION_METRICS as desktop };
@@ -42,6 +15,7 @@ export namespace nonSimulatedSettingsOverrides {
42
15
  let networkQuietThresholdMs: number;
43
16
  let cpuQuietThresholdMs: number;
44
17
  }
18
+ import * as Lantern from '../lib/lantern/lantern.js';
45
19
  /**
46
20
  * @type {Required<LH.SharedFlagsSettings['screenEmulation']>}
47
21
  */
@@ -24,6 +24,19 @@ const config = {
24
24
  'bf-cache',
25
25
  ],
26
26
  },
27
+ audits: [
28
+ // Lightrider does not support WebSocket connections.
29
+ // See https://github.com/GoogleChrome/lighthouse/issues/12459
30
+ {
31
+ path: 'errors-in-console',
32
+ options: {
33
+ ignoredPatterns: [
34
+ 'ERR_BLOCKED_BY_CLIENT.Inspector',
35
+ /^WebSocket connection to 'wss?:\/\/.*' failed:/,
36
+ ],
37
+ },
38
+ },
39
+ ],
27
40
  };
28
41
 
29
42
  export default config;
@@ -24,6 +24,19 @@ const config = {
24
24
  'bf-cache',
25
25
  ],
26
26
  },
27
+ audits: [
28
+ // Lightrider does not support WebSocket connections.
29
+ // See https://github.com/GoogleChrome/lighthouse/issues/12459
30
+ {
31
+ path: 'errors-in-console',
32
+ options: {
33
+ ignoredPatterns: [
34
+ 'ERR_BLOCKED_BY_CLIENT.Inspector',
35
+ /^WebSocket connection to 'wss?:\/\/.*' failed:/,
36
+ ],
37
+ },
38
+ },
39
+ ],
27
40
  };
28
41
 
29
42
  export default config;
@@ -5,12 +5,13 @@
5
5
  */
6
6
 
7
7
  import log from 'lighthouse-logger';
8
- import {isEqual} from 'lodash-es';
9
8
 
10
9
  import {
11
10
  getBrowserVersion, getBenchmarkIndex, getEnvironmentWarnings,
12
11
  getDevicePixelRatio,
13
12
  } from './driver/environment.js';
13
+ import {isEqual} from '../lib/arbitrary-equality-map.js';
14
+ import {Sentry} from '../lib/sentry.js';
14
15
 
15
16
  /**
16
17
  * @param {LH.Config.ResolvedConfig} resolvedConfig
@@ -23,6 +24,11 @@ async function getBaseArtifacts(resolvedConfig, driver, context) {
23
24
  const {userAgent, product} = await getBrowserVersion(driver.defaultSession);
24
25
  const HostDPR = await getDevicePixelRatio(driver.executionContext);
25
26
 
27
+ const [name, version] = product.split('/');
28
+ if (name && version) {
29
+ Sentry.setContext('browser', {name, version});
30
+ }
31
+
26
32
  return {
27
33
  // Meta artifacts.
28
34
  fetchTime: new Date().toJSON(),
@@ -41,13 +41,14 @@ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
41
41
  * @return {Promise<LH.IcuMessage[]>}
42
42
  */
43
43
  async function clearDataForOrigin(session, url, clearStorageTypes) {
44
+ const origin = new URL(url).origin;
45
+ if (origin === 'null') return [];
46
+
44
47
  const status = {msg: 'Cleaning origin data', id: 'lh:storage:clearDataForOrigin'};
45
48
  log.time(status);
46
49
 
47
50
  const warnings = [];
48
51
 
49
- const origin = new URL(url).origin;
50
-
51
52
  const typesToClear = clearStorageTypes.join(',');
52
53
 
53
54
  // `Storage.clearDataForOrigin` is one of our PROTOCOL_TIMEOUT culprits and this command is also