lighthouse 11.0.0 → 11.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/cli/bin.js +1 -1
  2. package/cli/cli-flags.js +1 -1
  3. package/cli/test/smokehouse/core-tests.js +3 -1
  4. package/cli/test/smokehouse/frontends/smokehouse-bin.js +1 -1
  5. package/cli/test/smokehouse/lighthouse-runners/bundle.js +1 -1
  6. package/cli/test/smokehouse/lighthouse-runners/cli.js +1 -1
  7. package/cli/test/smokehouse/lighthouse-runners/devtools.js +1 -1
  8. package/core/audits/accessibility/accesskeys.js +1 -1
  9. package/core/audits/accessibility/aria-allowed-attr.js +1 -1
  10. package/core/audits/accessibility/aria-allowed-role.js +1 -1
  11. package/core/audits/accessibility/aria-command-name.js +1 -1
  12. package/core/audits/accessibility/aria-dialog-name.js +1 -1
  13. package/core/audits/accessibility/aria-hidden-body.js +1 -1
  14. package/core/audits/accessibility/aria-hidden-focus.js +1 -1
  15. package/core/audits/accessibility/aria-input-field-name.js +1 -1
  16. package/core/audits/accessibility/aria-meter-name.js +1 -1
  17. package/core/audits/accessibility/aria-progressbar-name.js +1 -1
  18. package/core/audits/accessibility/aria-required-attr.js +1 -1
  19. package/core/audits/accessibility/aria-required-children.js +1 -1
  20. package/core/audits/accessibility/aria-required-parent.js +1 -1
  21. package/core/audits/accessibility/aria-roles.js +1 -1
  22. package/core/audits/accessibility/aria-text.js +1 -1
  23. package/core/audits/accessibility/aria-toggle-field-name.js +1 -1
  24. package/core/audits/accessibility/aria-tooltip-name.js +1 -1
  25. package/core/audits/accessibility/aria-treeitem-name.js +1 -1
  26. package/core/audits/accessibility/aria-valid-attr-value.js +1 -1
  27. package/core/audits/accessibility/aria-valid-attr.js +1 -1
  28. package/core/audits/accessibility/button-name.js +1 -1
  29. package/core/audits/accessibility/bypass.js +1 -1
  30. package/core/audits/accessibility/color-contrast.js +1 -1
  31. package/core/audits/accessibility/definition-list.js +1 -1
  32. package/core/audits/accessibility/dlitem.js +1 -1
  33. package/core/audits/accessibility/document-title.js +1 -1
  34. package/core/audits/accessibility/duplicate-id-active.js +1 -1
  35. package/core/audits/accessibility/duplicate-id-aria.js +2 -1
  36. package/core/audits/accessibility/empty-heading.js +1 -1
  37. package/core/audits/accessibility/form-field-multiple-labels.js +1 -1
  38. package/core/audits/accessibility/frame-title.js +1 -1
  39. package/core/audits/accessibility/heading-order.js +1 -1
  40. package/core/audits/accessibility/html-has-lang.js +1 -1
  41. package/core/audits/accessibility/html-lang-valid.js +1 -1
  42. package/core/audits/accessibility/html-xml-lang-mismatch.js +1 -1
  43. package/core/audits/accessibility/identical-links-same-purpose.js +1 -1
  44. package/core/audits/accessibility/image-alt.js +1 -1
  45. package/core/audits/accessibility/image-redundant-alt.js +1 -1
  46. package/core/audits/accessibility/input-button-name.js +1 -1
  47. package/core/audits/accessibility/input-image-alt.js +1 -1
  48. package/core/audits/accessibility/label-content-name-mismatch.js +1 -1
  49. package/core/audits/accessibility/label.js +1 -1
  50. package/core/audits/accessibility/landmark-one-main.js +1 -1
  51. package/core/audits/accessibility/link-in-text-block.js +1 -1
  52. package/core/audits/accessibility/link-name.js +1 -1
  53. package/core/audits/accessibility/list.js +1 -1
  54. package/core/audits/accessibility/listitem.js +1 -1
  55. package/core/audits/accessibility/meta-refresh.js +1 -1
  56. package/core/audits/accessibility/meta-viewport.js +1 -1
  57. package/core/audits/accessibility/object-alt.js +1 -1
  58. package/core/audits/accessibility/select-name.js +1 -1
  59. package/core/audits/accessibility/skip-link.js +1 -1
  60. package/core/audits/accessibility/tabindex.js +1 -1
  61. package/core/audits/accessibility/table-duplicate-name.js +1 -1
  62. package/core/audits/accessibility/table-fake-caption.js +1 -1
  63. package/core/audits/accessibility/target-size.js +1 -1
  64. package/core/audits/accessibility/td-has-header.js +1 -1
  65. package/core/audits/accessibility/td-headers-attr.js +1 -1
  66. package/core/audits/accessibility/th-has-data-cells.js +1 -1
  67. package/core/audits/accessibility/valid-lang.js +1 -1
  68. package/core/audits/accessibility/video-caption.js +1 -1
  69. package/core/audits/bf-cache.js +1 -0
  70. package/core/audits/bootup-time.d.ts +6 -0
  71. package/core/audits/bootup-time.js +37 -1
  72. package/core/audits/byte-efficiency/duplicated-javascript.js +1 -0
  73. package/core/audits/byte-efficiency/efficient-animated-content.js +1 -0
  74. package/core/audits/byte-efficiency/legacy-javascript.js +2 -1
  75. package/core/audits/byte-efficiency/modern-image-formats.js +1 -0
  76. package/core/audits/byte-efficiency/offscreen-images.js +1 -0
  77. package/core/audits/byte-efficiency/render-blocking-resources.js +1 -0
  78. package/core/audits/byte-efficiency/total-byte-weight.js +1 -0
  79. package/core/audits/byte-efficiency/unminified-css.js +1 -0
  80. package/core/audits/byte-efficiency/unminified-javascript.js +1 -0
  81. package/core/audits/byte-efficiency/unused-css-rules.js +1 -0
  82. package/core/audits/byte-efficiency/unused-javascript.js +1 -0
  83. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +1 -0
  84. package/core/audits/byte-efficiency/uses-optimized-images.js +1 -0
  85. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +1 -0
  86. package/core/audits/byte-efficiency/uses-responsive-images.js +1 -0
  87. package/core/audits/byte-efficiency/uses-text-compression.js +1 -0
  88. package/core/audits/critical-request-chains.js +1 -0
  89. package/core/audits/dobetterweb/dom-size.d.ts +8 -2
  90. package/core/audits/dobetterweb/dom-size.js +51 -3
  91. package/core/audits/dobetterweb/no-document-write.js +1 -0
  92. package/core/audits/dobetterweb/uses-http2.js +1 -0
  93. package/core/audits/dobetterweb/uses-passive-event-listeners.js +1 -0
  94. package/core/audits/font-display.js +1 -0
  95. package/core/audits/image-aspect-ratio.js +10 -1
  96. package/core/audits/installable-manifest.d.ts +0 -6
  97. package/core/audits/installable-manifest.js +1 -17
  98. package/core/audits/largest-contentful-paint-element.js +22 -4
  99. package/core/audits/layout-shift-elements.js +1 -0
  100. package/core/audits/lcp-lazy-loaded.d.ts +3 -2
  101. package/core/audits/lcp-lazy-loaded.js +35 -5
  102. package/core/audits/long-tasks.js +1 -0
  103. package/core/audits/mainthread-work-breakdown.js +1 -0
  104. package/core/audits/non-composited-animations.js +1 -0
  105. package/core/audits/prioritize-lcp-image.js +1 -0
  106. package/core/audits/redirects.js +2 -4
  107. package/core/audits/seo/crawlable-anchors.js +2 -1
  108. package/core/audits/server-response-time.js +1 -0
  109. package/core/audits/third-party-facades.js +14 -5
  110. package/core/audits/third-party-summary.d.ts +4 -2
  111. package/core/audits/third-party-summary.js +25 -11
  112. package/core/audits/unsized-images.js +1 -0
  113. package/core/audits/user-timings.js +1 -0
  114. package/core/audits/uses-rel-preconnect.js +1 -0
  115. package/core/audits/uses-rel-preload.js +1 -0
  116. package/core/audits/viewport.js +1 -0
  117. package/core/audits/work-during-interaction.js +1 -0
  118. package/core/computed/main-resource.d.ts +1 -1
  119. package/core/computed/main-resource.js +10 -4
  120. package/core/computed/page-dependency-graph.js +2 -1
  121. package/core/computed/tbt-impact-tasks.d.ts +8 -13
  122. package/core/computed/tbt-impact-tasks.js +4 -6
  123. package/core/computed/viewport-meta.js +11 -1
  124. package/core/config/config-helpers.js +1 -1
  125. package/core/config/config.js +1 -1
  126. package/core/gather/gatherers/accessibility.js +5 -0
  127. package/core/gather/timespan-runner.d.ts +3 -0
  128. package/core/gather/timespan-runner.js +23 -0
  129. package/core/index.cjs +13 -13
  130. package/core/index.d.cts +15 -2
  131. package/core/lib/asset-saver.d.ts +0 -3
  132. package/core/lib/asset-saver.js +68 -42
  133. package/core/lib/dependency-graph/simulator/network-analyzer.d.ts +7 -1
  134. package/core/lib/dependency-graph/simulator/network-analyzer.js +17 -1
  135. package/core/lib/emulation.js +1 -1
  136. package/core/lib/i18n/i18n.js +2 -2
  137. package/core/runner.js +5 -4
  138. package/dist/report/bundle.esm.js +3 -1
  139. package/dist/report/flow.js +3 -1
  140. package/dist/report/standalone.js +3 -1
  141. package/flow-report/tsconfig.json +0 -2
  142. package/package.json +8 -7
  143. package/readme.md +4 -1
  144. package/report/assets/templates.html +2 -0
  145. package/report/generator/flow-report-assets.js +1 -1
  146. package/report/generator/report-assets.js +1 -1
  147. package/report/generator/tsconfig.json +1 -1
  148. package/report/renderer/components.js +1 -1
  149. package/report/renderer/i18n-formatter.js +1 -1
  150. package/shared/localization/format.js +1 -1
  151. package/shared/localization/locales/ar-XB.json +36 -39
  152. package/shared/localization/locales/ar.json +36 -39
  153. package/shared/localization/locales/bg.json +36 -39
  154. package/shared/localization/locales/ca.json +36 -39
  155. package/shared/localization/locales/cs.json +36 -39
  156. package/shared/localization/locales/da.json +36 -39
  157. package/shared/localization/locales/de.json +36 -39
  158. package/shared/localization/locales/el.json +36 -39
  159. package/shared/localization/locales/en-GB.json +7 -10
  160. package/shared/localization/locales/en-US.json +64 -64
  161. package/shared/localization/locales/en-XA.json +36 -39
  162. package/shared/localization/locales/en-XL.json +64 -64
  163. package/shared/localization/locales/es-419.json +36 -39
  164. package/shared/localization/locales/es.json +36 -39
  165. package/shared/localization/locales/fi.json +36 -39
  166. package/shared/localization/locales/fil.json +36 -39
  167. package/shared/localization/locales/fr.json +36 -39
  168. package/shared/localization/locales/he.json +36 -39
  169. package/shared/localization/locales/hi.json +36 -39
  170. package/shared/localization/locales/hr.json +36 -39
  171. package/shared/localization/locales/hu.json +36 -39
  172. package/shared/localization/locales/id.json +36 -39
  173. package/shared/localization/locales/it.json +36 -39
  174. package/shared/localization/locales/ja.json +36 -39
  175. package/shared/localization/locales/ko.json +36 -39
  176. package/shared/localization/locales/lt.json +36 -39
  177. package/shared/localization/locales/lv.json +36 -39
  178. package/shared/localization/locales/nl.json +36 -39
  179. package/shared/localization/locales/no.json +36 -39
  180. package/shared/localization/locales/pl.json +36 -39
  181. package/shared/localization/locales/pt-PT.json +36 -39
  182. package/shared/localization/locales/pt.json +36 -39
  183. package/shared/localization/locales/ro.json +36 -39
  184. package/shared/localization/locales/ru.json +36 -39
  185. package/shared/localization/locales/sk.json +36 -39
  186. package/shared/localization/locales/sl.json +36 -39
  187. package/shared/localization/locales/sr-Latn.json +36 -39
  188. package/shared/localization/locales/sr.json +36 -39
  189. package/shared/localization/locales/sv.json +36 -39
  190. package/shared/localization/locales/ta.json +36 -39
  191. package/shared/localization/locales/te.json +36 -39
  192. package/shared/localization/locales/th.json +36 -39
  193. package/shared/localization/locales/tr.json +36 -39
  194. package/shared/localization/locales/uk.json +36 -39
  195. package/shared/localization/locales/vi.json +36 -39
  196. package/shared/localization/locales/zh-HK.json +36 -39
  197. package/shared/localization/locales/zh-TW.json +36 -39
  198. package/shared/localization/locales/zh.json +36 -39
  199. package/shared/localization/locales.js +1 -1
  200. package/{root.js → shared/root.js} +2 -1
  201. package/shared/tsconfig.json +0 -1
  202. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +1 -1
  203. package/third-party/chromium-synchronization/installability-errors-test.js +0 -1
  204. package/third-party/download-content-shell/download-content-shell.js +1 -1
  205. package/tsconfig.json +0 -2
  206. package/types/artifacts.d.ts +2 -1
  207. package/types/audit.d.ts +2 -0
  208. /package/{esm-utils.d.ts → shared/esm-utils.d.ts} +0 -0
  209. /package/{esm-utils.js → shared/esm-utils.js} +0 -0
  210. /package/{root.d.ts → shared/root.d.ts} +0 -0
@@ -87,17 +87,12 @@ const UIStrings = {
87
87
  /** Error message explaining that the web manifest's URL changed while the manifest was being downloaded by the browser. */
88
88
  'manifest-location-changed': `Manifest URL changed while the manifest was being fetched.`,
89
89
  /** Warning message explaining that the page does not work offline. */
90
+ // TODO(COMPAT): This error was removed in M118, we can remove this message when it hits stable.
90
91
  'warn-not-offline-capable': `Page does not work offline. The page will not be regarded as installable after Chrome 93, stable release August 2021.`,
91
92
  /** Error message explaining that Lighthouse failed while checking if the page is installable, and directing the user to try again in a new Chrome. */
92
93
  'protocol-timeout': `Lighthouse could not determine if the page is installable. Please try with a newer version of Chrome.`,
93
94
  /** Message logged when the web app has been uninstalled o desktop, signalling that the install banner state is being reset. */
94
95
  'pipeline-restarted': 'PWA has been uninstalled and installability checks resetting.',
95
- /**
96
- * TODO: This error was removed in M114, we can remove this message when it hits stable.
97
- * @description Error message explaining that the URL of the manifest uses a scheme that is not supported on Android.
98
- * @example {data:} scheme
99
- */
100
- 'scheme-not-supported-for-webapk': 'The manifest URL scheme ({scheme}) is not supported on Android.',
101
96
  };
102
97
  /* eslint-enable max-len */
103
98
 
@@ -157,17 +152,6 @@ class InstallableManifest extends Audit {
157
152
  // @ts-expect-error errorIds from protocol should match up against the strings dict
158
153
  const matchingString = UIStrings[err.errorId];
159
154
 
160
- if (err.errorId === 'scheme-not-supported-for-webapk') {
161
- // If there was no manifest, then there will be at lest one other installability error.
162
- // We can ignore this error if that's the case.
163
- const manifestUrl = artifacts.WebAppManifest?.url;
164
- if (!manifestUrl) continue;
165
-
166
- const scheme = new URL(manifestUrl).protocol;
167
- i18nErrors.push(str_(matchingString, {scheme}));
168
- continue;
169
- }
170
-
171
155
  // Handle an errorId we don't recognize.
172
156
  if (matchingString === undefined) {
173
157
  i18nErrors.push(str_(UIStrings.noErrorId, {errorId: err.errorId}));
@@ -8,7 +8,8 @@ import log from 'lighthouse-logger';
8
8
 
9
9
  import {Audit} from './audit.js';
10
10
  import * as i18n from '../lib/i18n/i18n.js';
11
- import {LargestContentfulPaint} from '../computed/metrics/largest-contentful-paint.js';
11
+ import {LargestContentfulPaint as LargestContentfulPaintComputed} from '../computed/metrics/largest-contentful-paint.js';
12
+ import LargestContentfulPaint from './metrics/largest-contentful-paint.js';
12
13
  import {LCPBreakdown} from '../computed/metrics/lcp-breakdown.js';
13
14
  import {Sentry} from '../lib/sentry.js';
14
15
 
@@ -46,6 +47,7 @@ class LargestContentfulPaintElement extends Audit {
46
47
  title: str_(UIStrings.title),
47
48
  description: str_(UIStrings.description),
48
49
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
50
+ guidanceLevel: 1,
49
51
  supportedModes: ['navigation'],
50
52
  requiredArtifacts:
51
53
  ['traces', 'TraceElements', 'devtoolsLogs', 'GatherContext', 'settings', 'URL'],
@@ -124,14 +126,22 @@ class LargestContentfulPaintElement extends Audit {
124
126
  settings: context.settings, URL: artifacts.URL};
125
127
 
126
128
  const elementTable = this.makeElementTable(artifacts);
127
- if (!elementTable) return {score: null, notApplicable: true};
129
+ if (!elementTable) {
130
+ return {
131
+ score: null,
132
+ notApplicable: true,
133
+ metricSavings: {LCP: 0},
134
+ };
135
+ }
128
136
 
129
137
  const items = [elementTable];
130
138
  let displayValue;
139
+ let metricLcp = 0;
131
140
 
132
141
  try {
133
- const {timing: metricLcp} =
134
- await LargestContentfulPaint.request(metricComputationData, context);
142
+ const lcpResult =
143
+ await LargestContentfulPaintComputed.request(metricComputationData, context);
144
+ metricLcp = lcpResult.timing;
135
145
  displayValue = str_(i18n.UIStrings.ms, {timeInMs: metricLcp});
136
146
 
137
147
  const phaseTable = await this.makePhaseTable(metricLcp, metricComputationData, context);
@@ -146,10 +156,18 @@ class LargestContentfulPaintElement extends Audit {
146
156
 
147
157
  const details = Audit.makeListDetails(items);
148
158
 
159
+ // Conceptually, this doesn't make much sense as "savings" for this audit since there isn't anything to "fix".
160
+ // However, this audit will always be useful when improving LCP and that should be reflected in our impact calculations.
161
+ const idealLcp = LargestContentfulPaint.defaultOptions[context.settings.formFactor].scoring.p10;
162
+ const lcpSavings = Math.max(0, metricLcp - idealLcp);
163
+
149
164
  return {
150
165
  score: 1,
151
166
  displayValue,
152
167
  details,
168
+ metricSavings: {
169
+ LCP: lcpSavings,
170
+ },
153
171
  };
154
172
  }
155
173
  }
@@ -29,6 +29,7 @@ class LayoutShiftElements extends Audit {
29
29
  title: str_(UIStrings.title),
30
30
  description: str_(UIStrings.description),
31
31
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
32
+ guidanceLevel: 2,
32
33
  requiredArtifacts: ['traces', 'TraceElements'],
33
34
  };
34
35
  }
@@ -8,9 +8,10 @@ declare class LargestContentfulPaintLazyLoaded extends Audit {
8
8
  static isImageInViewport(image: LH.Artifacts.ImageElement, viewportDimensions: LH.Artifacts.ViewportDimensions): boolean;
9
9
  /**
10
10
  * @param {LH.Artifacts} artifacts
11
- * @return {LH.Audit.Product}
11
+ * @param {LH.Audit.Context} context
12
+ * @return {Promise<LH.Audit.Product>}
12
13
  */
13
- static audit(artifacts: LH.Artifacts): LH.Audit.Product;
14
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
14
15
  }
15
16
  export namespace UIStrings {
16
17
  const title: string;
@@ -6,6 +6,8 @@
6
6
 
7
7
  import {Audit} from './audit.js';
8
8
  import * as i18n from '../lib/i18n/i18n.js';
9
+ import {LCPBreakdown} from '../computed/metrics/lcp-breakdown.js';
10
+ import {LargestContentfulPaint} from '../computed/metrics/largest-contentful-paint.js';
9
11
 
10
12
  const UIStrings = {
11
13
  /** Title of a Lighthouse audit that provides detail on whether the largest above-the-fold image was loaded with sufficient priority. This descriptive title is shown to users when the image was loaded properly. */
@@ -18,6 +20,8 @@ const UIStrings = {
18
20
 
19
21
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
20
22
 
23
+ const ESTIMATED_PERCENT_SAVINGS = 0.15;
24
+
21
25
  class LargestContentfulPaintLazyLoaded extends Audit {
22
26
  /**
23
27
  * @return {LH.Audit.Meta}
@@ -29,7 +33,9 @@ class LargestContentfulPaintLazyLoaded extends Audit {
29
33
  failureTitle: str_(UIStrings.failureTitle),
30
34
  description: str_(UIStrings.description),
31
35
  supportedModes: ['navigation'],
32
- requiredArtifacts: ['TraceElements', 'ViewportDimensions', 'ImageElements'],
36
+ guidanceLevel: 3,
37
+ requiredArtifacts: ['TraceElements', 'ViewportDimensions', 'ImageElements',
38
+ 'traces', 'devtoolsLogs', 'GatherContext', 'URL'],
33
39
  };
34
40
  }
35
41
 
@@ -46,9 +52,10 @@ class LargestContentfulPaintLazyLoaded extends Audit {
46
52
 
47
53
  /**
48
54
  * @param {LH.Artifacts} artifacts
49
- * @return {LH.Audit.Product}
55
+ * @param {LH.Audit.Context} context
56
+ * @return {Promise<LH.Audit.Product>}
50
57
  */
51
- static audit(artifacts) {
58
+ static async audit(artifacts, context) {
52
59
  const lcpElement = artifacts.TraceElements.find(element => {
53
60
  return element.traceEventType === 'largest-contentful-paint' && element.type === 'image';
54
61
  });
@@ -59,7 +66,11 @@ class LargestContentfulPaintLazyLoaded extends Audit {
59
66
 
60
67
  if (!lcpElementImage ||
61
68
  !this.isImageInViewport(lcpElementImage, artifacts.ViewportDimensions)) {
62
- return {score: null, notApplicable: true};
69
+ return {
70
+ score: null,
71
+ notApplicable: true,
72
+ metricSavings: {LCP: 0},
73
+ };
63
74
  }
64
75
 
65
76
  /** @type {LH.Audit.Details.Table['headings']} */
@@ -73,8 +84,27 @@ class LargestContentfulPaintLazyLoaded extends Audit {
73
84
  },
74
85
  ]);
75
86
 
87
+ const wasLazyLoaded = lcpElementImage.loading === 'lazy';
88
+
89
+ const metricComputationData = Audit.makeMetricComputationDataInput(artifacts, context);
90
+ const {timing: metricLcp} =
91
+ await LargestContentfulPaint.request(metricComputationData, context);
92
+ const lcpBreakdown = await LCPBreakdown.request(metricComputationData, context);
93
+ let lcpSavings = 0;
94
+ if (wasLazyLoaded && lcpBreakdown.loadStart !== undefined) {
95
+ // Estimate the LCP savings using a statistical percentage.
96
+ // https://web.dev/lcp-lazy-loading/#causal-performance
97
+ //
98
+ // LCP savings will be at most the LCP load delay.
99
+ const lcpLoadDelay = lcpBreakdown.loadStart - lcpBreakdown.ttfb;
100
+ lcpSavings = Math.min(metricLcp * ESTIMATED_PERCENT_SAVINGS, lcpLoadDelay);
101
+ }
102
+
76
103
  return {
77
- score: lcpElementImage.loading === 'lazy' ? 0 : 1,
104
+ score: wasLazyLoaded ? 0 : 1,
105
+ metricSavings: {
106
+ LCP: lcpSavings,
107
+ },
78
108
  details,
79
109
  };
80
110
  }
@@ -66,6 +66,7 @@ class LongTasks extends Audit {
66
66
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
67
67
  title: str_(UIStrings.title),
68
68
  description: str_(UIStrings.description),
69
+ guidanceLevel: 1,
69
70
  requiredArtifacts: ['traces', 'devtoolsLogs', 'URL'],
70
71
  };
71
72
  }
@@ -42,6 +42,7 @@ class MainThreadWorkBreakdown extends Audit {
42
42
  failureTitle: str_(UIStrings.failureTitle),
43
43
  description: str_(UIStrings.description),
44
44
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
45
+ guidanceLevel: 1,
45
46
  requiredArtifacts: ['traces'],
46
47
  };
47
48
  }
@@ -110,6 +110,7 @@ class NonCompositedAnimations extends Audit {
110
110
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
111
111
  title: str_(UIStrings.title),
112
112
  description: str_(UIStrings.description),
113
+ guidanceLevel: 2,
113
114
  requiredArtifacts: ['TraceElements', 'HostUserAgent'],
114
115
  };
115
116
  }
@@ -38,6 +38,7 @@ class PrioritizeLcpImage extends Audit {
38
38
  title: str_(UIStrings.title),
39
39
  description: str_(UIStrings.description),
40
40
  supportedModes: ['navigation'],
41
+ guidanceLevel: 3,
41
42
  requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext', 'URL', 'TraceElements'],
42
43
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
43
44
  };
@@ -31,6 +31,7 @@ class Redirects extends Audit {
31
31
  description: str_(UIStrings.description),
32
32
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
33
33
  supportedModes: ['navigation'],
34
+ guidanceLevel: 2,
34
35
  requiredArtifacts: ['URL', 'GatherContext', 'devtoolsLogs', 'traces'],
35
36
  };
36
37
  }
@@ -144,10 +145,7 @@ class Redirects extends Audit {
144
145
  {overallSavingsMs: totalWastedMs});
145
146
 
146
147
  return {
147
- // We award a passing grade if you only have 1 redirect
148
- // TODO(phulce): reconsider if cases like the example in https://github.com/GoogleChrome/lighthouse/issues/8984
149
- // should fail this audit.
150
- score: documentRequests.length <= 2 ? 1 : ByteEfficiencyAudit.scoreForWastedMs(totalWastedMs),
148
+ score: ByteEfficiencyAudit.scoreForWastedMs(totalWastedMs),
151
149
  numericValue: totalWastedMs,
152
150
  numericUnit: 'millisecond',
153
151
  displayValue: totalWastedMs ?
@@ -44,6 +44,7 @@ class CrawlableAnchors extends Audit {
44
44
  name = '',
45
45
  role = '',
46
46
  id,
47
+ href,
47
48
  }) => {
48
49
  rawHref = rawHref.replace( /\s/g, '');
49
50
  name = name.trim();
@@ -61,7 +62,7 @@ class CrawlableAnchors extends Audit {
61
62
  if (rawHref.startsWith('file:')) return true;
62
63
  if (name.length > 0) return;
63
64
 
64
- if (rawHref === '') return true;
65
+ if (href === '') return true;
65
66
  if (javaScriptVoidRegExp.test(rawHref)) return true;
66
67
 
67
68
  // checking if rawHref is a valid
@@ -37,6 +37,7 @@ class ServerResponseTime extends Audit {
37
37
  failureTitle: str_(UIStrings.failureTitle),
38
38
  description: str_(UIStrings.description),
39
39
  supportedModes: ['navigation'],
40
+ guidanceLevel: 1,
40
41
  requiredArtifacts: ['devtoolsLogs', 'URL', 'GatherContext'],
41
42
  };
42
43
  }
@@ -24,8 +24,8 @@ import * as i18n from '../lib/i18n/i18n.js';
24
24
  import {EntityClassification} from '../computed/entity-classification.js';
25
25
  import thirdPartyWeb from '../lib/third-party-web.js';
26
26
  import {NetworkRecords} from '../computed/network-records.js';
27
- import {MainThreadTasks} from '../computed/main-thread-tasks.js';
28
27
  import ThirdPartySummary from './third-party-summary.js';
28
+ import {TBTImpactTasks} from '../computed/tbt-impact-tasks.js';
29
29
 
30
30
  const UIStrings = {
31
31
  /** Title of a diagnostic audit that provides details about the third-party code on a web page that can be lazy loaded with a facade alternative. This descriptive title is shown to users when no resources have facade alternatives available. A facade is a lightweight component which looks like the desired resource. Lazy loading means resources are deferred until they are needed. Third-party code refers to resources that are not within the control of the site owner. */
@@ -86,7 +86,8 @@ class ThirdPartyFacades extends Audit {
86
86
  failureTitle: str_(UIStrings.failureTitle),
87
87
  description: str_(UIStrings.description),
88
88
  supportedModes: ['navigation'],
89
- requiredArtifacts: ['traces', 'devtoolsLogs', 'URL'],
89
+ guidanceLevel: 3,
90
+ requiredArtifacts: ['traces', 'devtoolsLogs', 'URL', 'GatherContext'],
90
91
  };
91
92
  }
92
93
 
@@ -148,19 +149,23 @@ class ThirdPartyFacades extends Audit {
148
149
  */
149
150
  static async audit(artifacts, context) {
150
151
  const settings = context.settings;
151
- const trace = artifacts.traces[Audit.DEFAULT_PASS];
152
152
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
153
153
  const networkRecords = await NetworkRecords.request(devtoolsLog, context);
154
154
  const classifiedEntities = await EntityClassification.request(
155
155
  {URL: artifacts.URL, devtoolsLog}, context);
156
- const tasks = await MainThreadTasks.request(trace, context);
156
+
157
+ const metricComputationData = Audit.makeMetricComputationDataInput(artifacts, context);
158
+ const tbtImpactTasks = await TBTImpactTasks.request(metricComputationData, context);
159
+
157
160
  const multiplier = settings.throttlingMethod === 'simulate' ?
158
161
  settings.throttling.cpuSlowdownMultiplier : 1;
159
- const summaries = ThirdPartySummary.getSummaries(networkRecords, tasks, multiplier,
162
+ const summaries = ThirdPartySummary.getSummaries(networkRecords, tbtImpactTasks, multiplier,
160
163
  classifiedEntities);
161
164
  const facadableProducts =
162
165
  ThirdPartyFacades.getProductsWithFacade(summaries.byURL, classifiedEntities);
163
166
 
167
+ let tbtImpact = 0;
168
+
164
169
  /** @type {LH.Audit.Details.TableItem[]} */
165
170
  const results = [];
166
171
  for (const {product, entity} of facadableProducts) {
@@ -179,6 +184,8 @@ class ThirdPartyFacades extends Audit {
179
184
  const entitySummary = summaries.byEntity.get(entity);
180
185
  if (!urls || !entitySummary) continue;
181
186
 
187
+ tbtImpact += entitySummary.tbtImpact;
188
+
182
189
  const items = Array.from(urls).map((url) => {
183
190
  const urlStats = summaries.byURL.get(url);
184
191
  return /** @type {import('./third-party-summary.js').URLSummary} */ ({url, ...urlStats});
@@ -198,6 +205,7 @@ class ThirdPartyFacades extends Audit {
198
205
  return {
199
206
  score: 1,
200
207
  notApplicable: true,
208
+ metricSavings: {TBT: 0},
201
209
  };
202
210
  }
203
211
 
@@ -216,6 +224,7 @@ class ThirdPartyFacades extends Audit {
216
224
  itemCount: results.length,
217
225
  }),
218
226
  details: Audit.makeTableDetails(headings, results),
227
+ metricSavings: {TBT: tbtImpact},
219
228
  };
220
229
  }
221
230
  }
@@ -3,10 +3,12 @@ export type Summary = {
3
3
  mainThreadTime: number;
4
4
  transferSize: number;
5
5
  blockingTime: number;
6
+ tbtImpact: number;
6
7
  };
7
8
  export type URLSummary = {
8
9
  transferSize: number;
9
10
  blockingTime: number;
11
+ tbtImpact: number;
10
12
  url: string | LH.IcuMessage;
11
13
  };
12
14
  export type SummaryMaps = {
@@ -27,12 +29,12 @@ declare class ThirdPartySummary extends Audit {
27
29
  /**
28
30
  *
29
31
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
30
- * @param {Array<LH.Artifacts.TaskNode>} mainThreadTasks
32
+ * @param {Array<LH.Artifacts.TBTImpactTask>} tbtImpactTasks
31
33
  * @param {number} cpuMultiplier
32
34
  * @param {LH.Artifacts.EntityClassification} entityClassification
33
35
  * @return {SummaryMaps}
34
36
  */
35
- static getSummaries(networkRecords: Array<LH.Artifacts.NetworkRequest>, mainThreadTasks: Array<LH.Artifacts.TaskNode>, cpuMultiplier: number, entityClassification: LH.Artifacts.EntityClassification): SummaryMaps;
37
+ static getSummaries(networkRecords: Array<LH.Artifacts.NetworkRequest>, tbtImpactTasks: Array<LH.Artifacts.TBTImpactTask>, cpuMultiplier: number, entityClassification: LH.Artifacts.EntityClassification): SummaryMaps;
36
38
  /**
37
39
  * @param {LH.Artifacts.Entity} entity
38
40
  * @param {SummaryMaps} summaries
@@ -8,8 +8,8 @@ import {Audit} from './audit.js';
8
8
  import {EntityClassification} from '../computed/entity-classification.js';
9
9
  import * as i18n from '../lib/i18n/i18n.js';
10
10
  import {NetworkRecords} from '../computed/network-records.js';
11
- import {MainThreadTasks} from '../computed/main-thread-tasks.js';
12
11
  import {getJavaScriptURLs, getAttributableURLForTask} from '../lib/tracehouse/task-summary.js';
12
+ import {TBTImpactTasks} from '../computed/tbt-impact-tasks.js';
13
13
 
14
14
  const UIStrings = {
15
15
  /** Title of a diagnostic audit that provides details about the code on a web page that the user doesn't control (referred to as "third-party code"). This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -38,12 +38,14 @@ const PASS_THRESHOLD_IN_MS = 250;
38
38
  * @property {number} mainThreadTime
39
39
  * @property {number} transferSize
40
40
  * @property {number} blockingTime
41
+ * @property {number} tbtImpact
41
42
  */
42
43
 
43
44
  /**
44
45
  * @typedef URLSummary
45
46
  * @property {number} transferSize
46
47
  * @property {number} blockingTime
48
+ * @property {number} tbtImpact
47
49
  * @property {string | LH.IcuMessage} url
48
50
  */
49
51
 
@@ -72,24 +74,25 @@ class ThirdPartySummary extends Audit {
72
74
  title: str_(UIStrings.title),
73
75
  failureTitle: str_(UIStrings.failureTitle),
74
76
  description: str_(UIStrings.description),
75
- requiredArtifacts: ['traces', 'devtoolsLogs', 'URL'],
77
+ guidanceLevel: 1,
78
+ requiredArtifacts: ['traces', 'devtoolsLogs', 'URL', 'GatherContext'],
76
79
  };
77
80
  }
78
81
 
79
82
  /**
80
83
  *
81
84
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
82
- * @param {Array<LH.Artifacts.TaskNode>} mainThreadTasks
85
+ * @param {Array<LH.Artifacts.TBTImpactTask>} tbtImpactTasks
83
86
  * @param {number} cpuMultiplier
84
87
  * @param {LH.Artifacts.EntityClassification} entityClassification
85
88
  * @return {SummaryMaps}
86
89
  */
87
- static getSummaries(networkRecords, mainThreadTasks, cpuMultiplier, entityClassification) {
90
+ static getSummaries(networkRecords, tbtImpactTasks, cpuMultiplier, entityClassification) {
88
91
  /** @type {Map<string, Summary>} */
89
92
  const byURL = new Map();
90
93
  /** @type {Map<LH.Artifacts.Entity, Summary>} */
91
94
  const byEntity = new Map();
92
- const defaultSummary = {mainThreadTime: 0, blockingTime: 0, transferSize: 0};
95
+ const defaultSummary = {mainThreadTime: 0, blockingTime: 0, transferSize: 0, tbtImpact: 0};
93
96
 
94
97
  for (const request of networkRecords) {
95
98
  const urlSummary = byURL.get(request.url) || {...defaultSummary};
@@ -99,7 +102,7 @@ class ThirdPartySummary extends Audit {
99
102
 
100
103
  const jsURLs = getJavaScriptURLs(networkRecords);
101
104
 
102
- for (const task of mainThreadTasks) {
105
+ for (const task of tbtImpactTasks) {
103
106
  const attributableURL = getAttributableURLForTask(task, jsURLs);
104
107
 
105
108
  const urlSummary = byURL.get(attributableURL) || {...defaultSummary};
@@ -110,6 +113,7 @@ class ThirdPartySummary extends Audit {
110
113
  // Note that this is not totally equivalent to the TBT definition since it fails to account for FCP,
111
114
  // but a majority of third-party work occurs after FCP and should yield largely similar numbers.
112
115
  urlSummary.blockingTime += Math.max(taskDuration - 50, 0);
116
+ urlSummary.tbtImpact += task.selfTbtImpact;
113
117
  byURL.set(attributableURL, urlSummary);
114
118
  }
115
119
 
@@ -127,6 +131,7 @@ class ThirdPartySummary extends Audit {
127
131
  entitySummary.transferSize += urlSummary.transferSize;
128
132
  entitySummary.mainThreadTime += urlSummary.mainThreadTime;
129
133
  entitySummary.blockingTime += urlSummary.blockingTime;
134
+ entitySummary.tbtImpact += urlSummary.tbtImpact;
130
135
  byEntity.set(entity, entitySummary);
131
136
 
132
137
  const entityURLs = urls.get(entity) || [];
@@ -152,7 +157,7 @@ class ThirdPartySummary extends Audit {
152
157
  // Sort by blocking time first, then transfer size to break ties.
153
158
  .sort((a, b) => (b.blockingTime - a.blockingTime) || (b.transferSize - a.transferSize));
154
159
 
155
- const subitemSummary = {transferSize: 0, blockingTime: 0};
160
+ const subitemSummary = {transferSize: 0, blockingTime: 0, tbtImpact: 0};
156
161
  const minTransferSize = Math.max(MIN_TRANSFER_SIZE_FOR_SUBITEMS, stats.transferSize / 20);
157
162
  const maxSubItems = Math.min(MAX_SUBITEMS, items.length);
158
163
  let numSubItems = 0;
@@ -167,6 +172,7 @@ class ThirdPartySummary extends Audit {
167
172
  numSubItems++;
168
173
  subitemSummary.transferSize += nextSubItem.transferSize;
169
174
  subitemSummary.blockingTime += nextSubItem.blockingTime;
175
+ subitemSummary.tbtImpact += nextSubItem.tbtImpact;
170
176
  }
171
177
  if (!subitemSummary.blockingTime && !subitemSummary.transferSize) {
172
178
  // Don't bother breaking down if there are no large resources.
@@ -179,6 +185,7 @@ class ThirdPartySummary extends Audit {
179
185
  url: str_(i18n.UIStrings.otherResourcesLabel),
180
186
  transferSize: stats.transferSize - subitemSummary.transferSize,
181
187
  blockingTime: stats.blockingTime - subitemSummary.blockingTime,
188
+ tbtImpact: stats.tbtImpact - subitemSummary.tbtImpact,
182
189
  };
183
190
  if (remainder.transferSize > minTransferSize) {
184
191
  items.push(remainder);
@@ -193,19 +200,21 @@ class ThirdPartySummary extends Audit {
193
200
  */
194
201
  static async audit(artifacts, context) {
195
202
  const settings = context.settings || {};
196
- const trace = artifacts.traces[Audit.DEFAULT_PASS];
197
203
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
198
204
  const networkRecords = await NetworkRecords.request(devtoolsLog, context);
199
205
  const classifiedEntities = await EntityClassification.request(
200
206
  {URL: artifacts.URL, devtoolsLog}, context);
201
207
  const firstPartyEntity = classifiedEntities.firstParty;
202
- const tasks = await MainThreadTasks.request(trace, context);
208
+
209
+ const metricComputationData = Audit.makeMetricComputationDataInput(artifacts, context);
210
+ const tbtImpactTasks = await TBTImpactTasks.request(metricComputationData, context);
211
+
203
212
  const multiplier = settings.throttlingMethod === 'simulate' ?
204
213
  settings.throttling.cpuSlowdownMultiplier : 1;
205
214
 
206
215
  const summaries = ThirdPartySummary.getSummaries(
207
- networkRecords, tasks, multiplier, classifiedEntities);
208
- const overallSummary = {wastedBytes: 0, wastedMs: 0};
216
+ networkRecords, tbtImpactTasks, multiplier, classifiedEntities);
217
+ const overallSummary = {wastedBytes: 0, wastedMs: 0, tbtImpact: 0};
209
218
 
210
219
  const results = Array.from(summaries.byEntity.entries())
211
220
  // Don't consider the page we're on to be third-party.
@@ -214,6 +223,7 @@ class ThirdPartySummary extends Audit {
214
223
  .map(([entity, stats]) => {
215
224
  overallSummary.wastedBytes += stats.transferSize;
216
225
  overallSummary.wastedMs += stats.blockingTime;
226
+ overallSummary.tbtImpact += stats.tbtImpact;
217
227
 
218
228
  return {
219
229
  ...stats,
@@ -240,6 +250,7 @@ class ThirdPartySummary extends Audit {
240
250
  return {
241
251
  score: 1,
242
252
  notApplicable: true,
253
+ metricSavings: {TBT: 0},
243
254
  };
244
255
  }
245
256
 
@@ -252,6 +263,9 @@ class ThirdPartySummary extends Audit {
252
263
  timeInMs: overallSummary.wastedMs,
253
264
  }),
254
265
  details,
266
+ metricSavings: {
267
+ TBT: overallSummary.tbtImpact,
268
+ },
255
269
  };
256
270
  }
257
271
  }
@@ -34,6 +34,7 @@ class UnsizedImages extends Audit {
34
34
  title: str_(UIStrings.title),
35
35
  failureTitle: str_(UIStrings.failureTitle),
36
36
  description: str_(UIStrings.description),
37
+ guidanceLevel: 3,
37
38
  requiredArtifacts: ['ImageElements'],
38
39
  };
39
40
  }
@@ -39,6 +39,7 @@ class UserTimings extends Audit {
39
39
  title: str_(UIStrings.title),
40
40
  description: str_(UIStrings.description),
41
41
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
42
+ guidanceLevel: 2,
42
43
  requiredArtifacts: ['traces'],
43
44
  };
44
45
  }
@@ -62,6 +62,7 @@ class UsesRelPreconnectAudit extends Audit {
62
62
  title: str_(UIStrings.title),
63
63
  description: str_(UIStrings.description),
64
64
  supportedModes: ['navigation'],
65
+ guidanceLevel: 3,
65
66
  requiredArtifacts: ['traces', 'devtoolsLogs', 'URL', 'LinkElements'],
66
67
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
67
68
  };
@@ -43,6 +43,7 @@ class UsesRelPreloadAudit extends Audit {
43
43
  title: str_(UIStrings.title),
44
44
  description: str_(UIStrings.description),
45
45
  supportedModes: ['navigation'],
46
+ guidanceLevel: 3,
46
47
  requiredArtifacts: ['devtoolsLogs', 'traces', 'URL'],
47
48
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
48
49
  };
@@ -34,6 +34,7 @@ class Viewport extends Audit {
34
34
  title: str_(UIStrings.title),
35
35
  failureTitle: str_(UIStrings.failureTitle),
36
36
  description: str_(UIStrings.description),
37
+ guidanceLevel: 3,
37
38
  requiredArtifacts: ['MetaElements'],
38
39
  };
39
40
  }
@@ -60,6 +60,7 @@ class WorkDuringInteraction extends Audit {
60
60
  description: str_(UIStrings.description),
61
61
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
62
62
  supportedModes: ['timespan'],
63
+ guidanceLevel: 1,
63
64
  requiredArtifacts: ['traces', 'devtoolsLogs', 'TraceElements'],
64
65
  };
65
66
  }
@@ -9,7 +9,7 @@ declare const MainResourceComputed: typeof MainResource & {
9
9
  };
10
10
  /**
11
11
  * @fileoverview This artifact identifies the main resource on the page. Current solution assumes
12
- * that the main resource is the first non-rediected one.
12
+ * that the main resource is the first non-redirected one.
13
13
  */
14
14
  declare class MainResource {
15
15
  /**
@@ -5,12 +5,12 @@
5
5
  */
6
6
 
7
7
  import {makeComputedArtifact} from './computed-artifact.js';
8
- import {NetworkAnalyzer} from '../lib/dependency-graph/simulator/network-analyzer.js';
9
8
  import {NetworkRecords} from './network-records.js';
9
+ import {NetworkAnalyzer} from '../lib/dependency-graph/simulator/network-analyzer.js';
10
10
 
11
11
  /**
12
12
  * @fileoverview This artifact identifies the main resource on the page. Current solution assumes
13
- * that the main resource is the first non-rediected one.
13
+ * that the main resource is the first non-redirected one.
14
14
  */
15
15
  class MainResource {
16
16
  /**
@@ -21,8 +21,14 @@ class MainResource {
21
21
  static async compute_(data, context) {
22
22
  const {mainDocumentUrl} = data.URL;
23
23
  if (!mainDocumentUrl) throw new Error('mainDocumentUrl must exist to get the main resource');
24
- const requests = await NetworkRecords.request(data.devtoolsLog, context);
25
- const mainResource = NetworkAnalyzer.findResourceForUrl(requests, mainDocumentUrl);
24
+ const records = await NetworkRecords.request(data.devtoolsLog, context);
25
+
26
+ // We could have more than one record matching the main doucment url,
27
+ // if the page did `location.reload()`. Since `mainDocumentUrl` refers to the _last_
28
+ // document request, we should return the last candidate here. Besides, the browser
29
+ // would have evicted the first request by the time `MainDocumentRequest` (a consumer
30
+ // of this computed artifact) attempts to fetch the contents, resulting in a protocol error.
31
+ const mainResource = NetworkAnalyzer.findLastDocumentForUrl(records, mainDocumentUrl);
26
32
  if (!mainResource) {
27
33
  throw new Error('Unable to identify the main resource');
28
34
  }
@@ -412,7 +412,8 @@ class PageDependencyGraph {
412
412
  const rootNode = networkNodeOutput.idToNodeMap.get(rootRequest.requestId);
413
413
  if (!rootNode) throw new Error('rootNode not found');
414
414
 
415
- const mainDocumentRequest = NetworkAnalyzer.findResourceForUrl(networkRecords, mainDocumentUrl);
415
+ const mainDocumentRequest =
416
+ NetworkAnalyzer.findLastDocumentForUrl(networkRecords, mainDocumentUrl);
416
417
  if (!mainDocumentRequest) throw new Error('mainDocumentRequest not found');
417
418
  const mainDocumentNode = networkNodeOutput.idToNodeMap.get(mainDocumentRequest.requestId);
418
419
  if (!mainDocumentNode) throw new Error('mainDocumentNode not found');