lighthouse 9.5.0-dev.20230206 → 9.5.0-dev.20230208

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 (78) hide show
  1. package/core/audits/audit.d.ts +12 -5
  2. package/core/audits/audit.js +19 -5
  3. package/core/audits/byte-efficiency/byte-efficiency-audit.js +2 -1
  4. package/core/audits/byte-efficiency/render-blocking-resources.js +2 -1
  5. package/core/audits/dobetterweb/js-libraries.js +1 -1
  6. package/core/audits/dobetterweb/uses-http2.js +2 -1
  7. package/core/audits/{preload-lcp-image.d.ts → prioritize-lcp-image.d.ts} +3 -3
  8. package/core/audits/{preload-lcp-image.js → prioritize-lcp-image.js} +14 -12
  9. package/core/audits/redirects.js +2 -1
  10. package/core/audits/seo/link-text.js +1 -1
  11. package/core/audits/seo/robots-txt.js +1 -1
  12. package/core/audits/server-response-time.js +1 -1
  13. package/core/audits/uses-rel-preconnect.js +2 -1
  14. package/core/audits/uses-rel-preload.js +4 -2
  15. package/core/audits/valid-source-maps.js +7 -3
  16. package/core/config/config.js +20 -1
  17. package/core/config/default-config.js +42 -90
  18. package/core/config/metrics-to-audits.js +1 -1
  19. package/core/gather/gatherers/full-page-screenshot.js +6 -2
  20. package/core/gather/gatherers/trace-elements.js +2 -1
  21. package/core/gather/navigation-runner.d.ts +4 -2
  22. package/core/gather/navigation-runner.js +16 -4
  23. package/core/legacy/config/config.js +18 -0
  24. package/core/lib/page-functions.js +2 -2
  25. package/dist/report/bundle.esm.js +1 -1
  26. package/dist/report/flow.js +11 -11
  27. package/dist/report/standalone.js +1 -1
  28. package/package.json +2 -2
  29. package/report/assets/styles.css +6 -3
  30. package/report/renderer/components.js +1 -1
  31. package/shared/localization/locales/ar-XB.json +982 -289
  32. package/shared/localization/locales/ar.json +983 -290
  33. package/shared/localization/locales/bg.json +981 -288
  34. package/shared/localization/locales/ca.json +988 -295
  35. package/shared/localization/locales/cs.json +983 -290
  36. package/shared/localization/locales/da.json +982 -289
  37. package/shared/localization/locales/de.json +1118 -425
  38. package/shared/localization/locales/el.json +981 -288
  39. package/shared/localization/locales/en-GB.json +982 -289
  40. package/shared/localization/locales/en-US.json +4 -4
  41. package/shared/localization/locales/en-XA.json +982 -289
  42. package/shared/localization/locales/en-XL.json +4 -4
  43. package/shared/localization/locales/es-419.json +984 -291
  44. package/shared/localization/locales/es.json +989 -296
  45. package/shared/localization/locales/fi.json +982 -289
  46. package/shared/localization/locales/fil.json +983 -290
  47. package/shared/localization/locales/fr.json +984 -291
  48. package/shared/localization/locales/he.json +1002 -309
  49. package/shared/localization/locales/hi.json +985 -292
  50. package/shared/localization/locales/hr.json +982 -289
  51. package/shared/localization/locales/hu.json +981 -288
  52. package/shared/localization/locales/id.json +982 -289
  53. package/shared/localization/locales/it.json +1002 -309
  54. package/shared/localization/locales/ja.json +981 -288
  55. package/shared/localization/locales/ko.json +982 -289
  56. package/shared/localization/locales/lt.json +982 -289
  57. package/shared/localization/locales/lv.json +982 -289
  58. package/shared/localization/locales/nl.json +983 -290
  59. package/shared/localization/locales/no.json +986 -293
  60. package/shared/localization/locales/pl.json +981 -288
  61. package/shared/localization/locales/pt-PT.json +1002 -309
  62. package/shared/localization/locales/pt.json +983 -290
  63. package/shared/localization/locales/ro.json +981 -288
  64. package/shared/localization/locales/ru.json +981 -288
  65. package/shared/localization/locales/sk.json +981 -288
  66. package/shared/localization/locales/sl.json +981 -288
  67. package/shared/localization/locales/sr-Latn.json +982 -289
  68. package/shared/localization/locales/sr.json +982 -289
  69. package/shared/localization/locales/sv.json +988 -295
  70. package/shared/localization/locales/ta.json +982 -289
  71. package/shared/localization/locales/te.json +1012 -319
  72. package/shared/localization/locales/th.json +990 -297
  73. package/shared/localization/locales/tr.json +982 -289
  74. package/shared/localization/locales/uk.json +981 -288
  75. package/shared/localization/locales/vi.json +983 -290
  76. package/shared/localization/locales/zh-HK.json +982 -289
  77. package/shared/localization/locales/zh-TW.json +985 -292
  78. package/shared/localization/locales/zh.json +982 -289
@@ -1,3 +1,11 @@
1
+ export type TableOptions = {
2
+ wastedMs?: number | undefined;
3
+ wastedBytes?: number | undefined;
4
+ };
5
+ export type OpportunityOptions = {
6
+ overallSavingsMs: number;
7
+ overallSavingsBytes?: number | undefined;
8
+ };
1
9
  export class Audit {
2
10
  /**
3
11
  * @return {string}
@@ -55,10 +63,10 @@ export class Audit {
55
63
  /**
56
64
  * @param {LH.Audit.Details.Table['headings']} headings
57
65
  * @param {LH.Audit.Details.Table['items']} results
58
- * @param {LH.Audit.Details.Table['summary']=} summary
66
+ * @param {TableOptions=} options
59
67
  * @return {LH.Audit.Details.Table}
60
68
  */
61
- static makeTableDetails(headings: LH.Audit.Details.Table['headings'], results: LH.Audit.Details.Table['items'], summary?: LH.Audit.Details.Table['summary'] | undefined): LH.Audit.Details.Table;
69
+ static makeTableDetails(headings: LH.Audit.Details.Table['headings'], results: LH.Audit.Details.Table['items'], options?: TableOptions | undefined): LH.Audit.Details.Table;
62
70
  /**
63
71
  * @param {LH.Audit.Details.List['items']} items
64
72
  * @return {LH.Audit.Details.List}
@@ -95,11 +103,10 @@ export class Audit {
95
103
  /**
96
104
  * @param {LH.Audit.Details.Opportunity['headings']} headings
97
105
  * @param {LH.Audit.Details.Opportunity['items']} items
98
- * @param {number} overallSavingsMs
99
- * @param {number=} overallSavingsBytes
106
+ * @param {OpportunityOptions} options
100
107
  * @return {LH.Audit.Details.Opportunity}
101
108
  */
102
- static makeOpportunityDetails(headings: LH.Audit.Details.Opportunity['headings'], items: LH.Audit.Details.Opportunity['items'], overallSavingsMs: number, overallSavingsBytes?: number | undefined): LH.Audit.Details.Opportunity;
109
+ static makeOpportunityDetails(headings: LH.Audit.Details.Opportunity['headings'], items: LH.Audit.Details.Opportunity['items'], options: OpportunityOptions): LH.Audit.Details.Opportunity;
103
110
  /**
104
111
  * @param {LH.Artifacts.NodeDetails} node
105
112
  * @return {LH.Audit.Details.NodeValue}
@@ -11,6 +11,18 @@ import {Util} from '../../shared/util.js';
11
11
 
12
12
  const DEFAULT_PASS = 'defaultPass';
13
13
 
14
+ /**
15
+ * @typedef TableOptions
16
+ * @property {number=} wastedMs
17
+ * @property {number=} wastedBytes
18
+ */
19
+
20
+ /**
21
+ * @typedef OpportunityOptions
22
+ * @property {number} overallSavingsMs
23
+ * @property {number=} overallSavingsBytes
24
+ */
25
+
14
26
  /**
15
27
  * Clamp figure to 2 decimal places
16
28
  * @param {number} val
@@ -123,10 +135,12 @@ class Audit {
123
135
  /**
124
136
  * @param {LH.Audit.Details.Table['headings']} headings
125
137
  * @param {LH.Audit.Details.Table['items']} results
126
- * @param {LH.Audit.Details.Table['summary']=} summary
138
+ * @param {TableOptions=} options
127
139
  * @return {LH.Audit.Details.Table}
128
140
  */
129
- static makeTableDetails(headings, results, summary) {
141
+ static makeTableDetails(headings, results, options = {}) {
142
+ const {wastedBytes, wastedMs} = options;
143
+ const summary = (wastedBytes || wastedMs) ? {wastedBytes, wastedMs} : undefined;
130
144
  if (results.length === 0) {
131
145
  return {
132
146
  type: 'table',
@@ -215,12 +229,12 @@ class Audit {
215
229
  /**
216
230
  * @param {LH.Audit.Details.Opportunity['headings']} headings
217
231
  * @param {LH.Audit.Details.Opportunity['items']} items
218
- * @param {number} overallSavingsMs
219
- * @param {number=} overallSavingsBytes
232
+ * @param {OpportunityOptions} options
220
233
  * @return {LH.Audit.Details.Opportunity}
221
234
  */
222
- static makeOpportunityDetails(headings, items, overallSavingsMs, overallSavingsBytes) {
235
+ static makeOpportunityDetails(headings, items, options) {
223
236
  Audit.assertHeadingKeysExist(headings, items);
237
+ const {overallSavingsMs, overallSavingsBytes} = options;
224
238
 
225
239
  return {
226
240
  type: 'opportunity',
@@ -228,7 +228,8 @@ class ByteEfficiencyAudit extends Audit {
228
228
  displayValue = str_(i18n.UIStrings.displayValueByteSavings, {wastedBytes});
229
229
  }
230
230
 
231
- const details = Audit.makeOpportunityDetails(result.headings, results, wastedMs, wastedBytes);
231
+ const details = Audit.makeOpportunityDetails(result.headings, results,
232
+ {overallSavingsMs: wastedMs, overallSavingsBytes: wastedBytes});
232
233
 
233
234
  return {
234
235
  explanation: result.explanation,
@@ -290,7 +290,8 @@ class RenderBlockingResources extends Audit {
290
290
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
291
291
  ];
292
292
 
293
- const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
293
+ const details = Audit.makeOpportunityDetails(headings, results,
294
+ {overallSavingsMs: wastedMs});
294
295
 
295
296
  return {
296
297
  displayValue,
@@ -57,7 +57,7 @@ class JsLibrariesAudit extends Audit {
57
57
  {key: 'name', valueType: 'text', label: str_(i18n.UIStrings.columnName)},
58
58
  {key: 'version', valueType: 'text', label: str_(UIStrings.columnVersion)},
59
59
  ];
60
- const details = Audit.makeTableDetails(headings, libDetails, {});
60
+ const details = Audit.makeTableDetails(headings, libDetails);
61
61
 
62
62
  const debugData = {
63
63
  type: /** @type {const} */ ('debugdata'),
@@ -247,7 +247,8 @@ class UsesHTTP2Audit extends Audit {
247
247
  {key: 'protocol', valueType: 'text', label: str_(UIStrings.columnProtocol)},
248
248
  ];
249
249
 
250
- const details = Audit.makeOpportunityDetails(headings, resources, wastedMs);
250
+ const details = Audit.makeOpportunityDetails(headings, resources,
251
+ {overallSavingsMs: wastedMs});
251
252
 
252
253
  return {
253
254
  displayValue,
@@ -1,4 +1,4 @@
1
- export default PreloadLCPImageAudit;
1
+ export default PrioritizeLcpImage;
2
2
  export type InitiatorPath = Array<{
3
3
  url: string;
4
4
  initiatorType: string;
@@ -6,7 +6,7 @@ export type InitiatorPath = Array<{
6
6
  /**
7
7
  * @typedef {Array<{url: string, initiatorType: string}>} InitiatorPath
8
8
  */
9
- declare class PreloadLCPImageAudit extends Audit {
9
+ declare class PrioritizeLcpImage extends Audit {
10
10
  /**
11
11
  *
12
12
  * @param {LH.Artifacts.NetworkRequest} request
@@ -73,4 +73,4 @@ export namespace UIStrings {
73
73
  import { Audit } from "./audit.js";
74
74
  import { NetworkRequest } from "../lib/network-request.js";
75
75
  import { ProcessedNavigation } from "../computed/processed-navigation.js";
76
- //# sourceMappingURL=preload-lcp-image.d.ts.map
76
+ //# sourceMappingURL=prioritize-lcp-image.d.ts.map
@@ -27,13 +27,13 @@ const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
27
27
  * @typedef {Array<{url: string, initiatorType: string}>} InitiatorPath
28
28
  */
29
29
 
30
- class PreloadLCPImageAudit extends Audit {
30
+ class PrioritizeLcpImage extends Audit {
31
31
  /**
32
32
  * @return {LH.Audit.Meta}
33
33
  */
34
34
  static get meta() {
35
35
  return {
36
- id: 'preload-lcp-image',
36
+ id: 'prioritize-lcp-image',
37
37
  title: str_(UIStrings.title),
38
38
  description: str_(UIStrings.description),
39
39
  supportedModes: ['navigation'],
@@ -92,11 +92,11 @@ class PreloadLCPImageAudit extends Audit {
92
92
  */
93
93
  static getLCPNodeToPreload(mainResource, graph, lcpUrl) {
94
94
  if (!lcpUrl) return {};
95
- const {lcpNode, path} = PreloadLCPImageAudit.findLCPNode(graph, lcpUrl);
95
+ const {lcpNode, path} = PrioritizeLcpImage.findLCPNode(graph, lcpUrl);
96
96
  if (!lcpNode || !path) return {};
97
97
 
98
98
  // eslint-disable-next-line max-len
99
- const shouldPreload = PreloadLCPImageAudit.shouldPreloadRequest(lcpNode.record, mainResource, path);
99
+ const shouldPreload = PrioritizeLcpImage.shouldPreloadRequest(lcpNode.record, mainResource, path);
100
100
  const lcpNodeToPreload = shouldPreload ? lcpNode : undefined;
101
101
 
102
102
  const initiatorPath = [
@@ -119,7 +119,8 @@ class PreloadLCPImageAudit extends Audit {
119
119
  * @return {string | undefined}
120
120
  */
121
121
  static getLcpUrl(trace, processedNavigation) {
122
- const lcpEvent = processedNavigation.largestContentfulPaintAllFramesEvt;
122
+ // Use main-frame-only LCP to match the metric value.
123
+ const lcpEvent = processedNavigation.largestContentfulPaintEvt;
123
124
  if (!lcpEvent) return;
124
125
 
125
126
  const lcpImagePaintEvent = trace.traceEvents.filter(e => {
@@ -229,8 +230,8 @@ class PreloadLCPImageAudit extends Audit {
229
230
  */
230
231
  static async audit(artifacts, context) {
231
232
  const gatherContext = artifacts.GatherContext;
232
- const trace = artifacts.traces[PreloadLCPImageAudit.DEFAULT_PASS];
233
- const devtoolsLog = artifacts.devtoolsLogs[PreloadLCPImageAudit.DEFAULT_PASS];
233
+ const trace = artifacts.traces[PrioritizeLcpImage.DEFAULT_PASS];
234
+ const devtoolsLog = artifacts.devtoolsLogs[PrioritizeLcpImage.DEFAULT_PASS];
234
235
  const URL = artifacts.URL;
235
236
  const metricData = {trace, devtoolsLog, gatherContext, settings: context.settings, URL};
236
237
  const lcpElement = artifacts.TraceElements
@@ -247,13 +248,13 @@ class PreloadLCPImageAudit extends Audit {
247
248
  LoadSimulator.request({devtoolsLog, settings: context.settings}, context),
248
249
  ]);
249
250
 
250
- const lcpUrl = PreloadLCPImageAudit.getLcpUrl(trace, processedNavigation);
251
+ const lcpUrl = PrioritizeLcpImage.getLcpUrl(trace, processedNavigation);
251
252
  const graph = lanternLCP.pessimisticGraph;
252
253
  // eslint-disable-next-line max-len
253
- const {lcpNodeToPreload, initiatorPath} = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpUrl);
254
+ const {lcpNodeToPreload, initiatorPath} = PrioritizeLcpImage.getLCPNodeToPreload(mainResource, graph, lcpUrl);
254
255
 
255
256
  const {results, wastedMs} =
256
- PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
257
+ PrioritizeLcpImage.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
257
258
 
258
259
  /** @type {LH.Audit.Details.Opportunity['headings']} */
259
260
  const headings = [
@@ -261,7 +262,8 @@ class PreloadLCPImageAudit extends Audit {
261
262
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
262
263
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
263
264
  ];
264
- const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
265
+ const details = Audit.makeOpportunityDetails(headings, results,
266
+ {overallSavingsMs: wastedMs});
265
267
 
266
268
  // If LCP element was an image and had valid network records (regardless of
267
269
  // if it should be preloaded), it will be found first in the `initiatorPath`.
@@ -284,5 +286,5 @@ class PreloadLCPImageAudit extends Audit {
284
286
  }
285
287
  }
286
288
 
287
- export default PreloadLCPImageAudit;
289
+ export default PrioritizeLcpImage;
288
290
  export {UIStrings};
@@ -142,7 +142,8 @@ class Redirects extends Audit {
142
142
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
143
143
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnTimeSpent)},
144
144
  ];
145
- const details = Audit.makeOpportunityDetails(headings, tableRows, totalWastedMs);
145
+ const details = Audit.makeOpportunityDetails(headings, tableRows,
146
+ {overallSavingsMs: totalWastedMs});
146
147
 
147
148
  return {
148
149
  // We award a passing grade if you only have 1 redirect
@@ -141,7 +141,7 @@ class LinkText extends Audit {
141
141
  {key: 'text', valueType: 'text', label: 'Link Text'},
142
142
  ];
143
143
 
144
- const details = Audit.makeTableDetails(headings, failingLinks, {});
144
+ const details = Audit.makeTableDetails(headings, failingLinks);
145
145
  let displayValue;
146
146
 
147
147
  if (failingLinks.length) {
@@ -235,7 +235,7 @@ class RobotsTxt extends Audit {
235
235
  {key: 'message', valueType: 'code', label: 'Error'},
236
236
  ];
237
237
 
238
- const details = Audit.makeTableDetails(headings, validationErrors, {});
238
+ const details = Audit.makeTableDetails(headings, validationErrors);
239
239
  let displayValue;
240
240
 
241
241
  if (validationErrors.length) {
@@ -73,7 +73,7 @@ class ServerResponseTime extends Audit {
73
73
  const details = Audit.makeOpportunityDetails(
74
74
  headings,
75
75
  [{url: mainResource.url, responseTime}],
76
- responseTime - TARGET_MS
76
+ {overallSavingsMs: responseTime - TARGET_MS}
77
77
  );
78
78
 
79
79
  return {
@@ -233,7 +233,8 @@ class UsesRelPreconnectAudit extends Audit {
233
233
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
234
234
  ];
235
235
 
236
- const details = Audit.makeOpportunityDetails(headings, results, maxWasted);
236
+ const details = Audit.makeOpportunityDetails(headings, results,
237
+ {overallSavingsMs: maxWasted});
237
238
 
238
239
  return {
239
240
  score: ByteEfficiencyAudit.scoreForWastedMs(maxWasted),
@@ -239,7 +239,8 @@ class UsesRelPreloadAudit extends Audit {
239
239
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
240
240
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
241
241
  ];
242
- const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
242
+ const details = Audit.makeOpportunityDetails(headings, results,
243
+ {overallSavingsMs: wastedMs});
243
244
 
244
245
  return {
245
246
  score: ByteEfficiencyAudit.scoreForWastedMs(wastedMs),
@@ -259,7 +260,8 @@ class UsesRelPreloadAudit extends Audit {
259
260
  static async audit() {
260
261
  // Preload advice is on hold until https://github.com/GoogleChrome/lighthouse/issues/11960
261
262
  // is resolved.
262
- return {score: 1, notApplicable: true, details: Audit.makeOpportunityDetails([], [], 0)};
263
+ return {score: 1, notApplicable: true,
264
+ details: Audit.makeOpportunityDetails([], [], {overallSavingsMs: 0})};
263
265
  }
264
266
  }
265
267
 
@@ -7,6 +7,8 @@
7
7
  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
+ import {Util} from '../../shared/util.js';
11
+ import UrlUtils from '../lib/url-utils.js';
10
12
 
11
13
  const UIStrings = {
12
14
  /** Title of a Lighthouse audit that provides detail on HTTP to HTTPS redirects. This descriptive title is shown to users when HTTP traffic is redirected to HTTPS. */
@@ -54,11 +56,13 @@ class ValidSourceMaps extends Audit {
54
56
  * @return {boolean}
55
57
  */
56
58
  static isLargeFirstPartyJS(script, classifiedEntities) {
57
- if (!script.length) return false;
59
+ const url = script.url;
60
+ if (!script.length || !url) return false;
61
+ if (!UrlUtils.isValid(url)) return false;
62
+ if (!Util.createOrReturnURL(url).protocol.startsWith('http')) return false;
58
63
 
59
64
  const isLargeJS = script.length >= LARGE_JS_BYTE_THRESHOLD;
60
- const isFirstPartyJS = script.url ? classifiedEntities.isFirstParty(script.url) : false;
61
- return isLargeJS && isFirstPartyJS;
65
+ return classifiedEntities.isFirstParty(url) && isLargeJS;
62
66
  }
63
67
 
64
68
  /**
@@ -37,6 +37,18 @@ const defaultConfigPath = path.join(
37
37
  '../../config/default-config.js'
38
38
  );
39
39
 
40
+ /**
41
+ * Certain gatherers are destructive to the page state.
42
+ * We should ensure that these gatherers run after any custom gatherers.
43
+ * The default priority should be 0.
44
+ * TODO: Make this an official part of the config or design a different solution.
45
+ * @type {Record<string, number|undefined>}
46
+ */
47
+ const internalArtifactPriorities = {
48
+ FullPageScreenshot: 1,
49
+ BFCacheFailures: 1,
50
+ };
51
+
40
52
  /**
41
53
  * @param {LH.Config|undefined} config
42
54
  * @param {{configPath?: string}} context
@@ -129,12 +141,19 @@ async function resolveArtifactsToDefns(artifacts, configDir) {
129
141
  const status = {msg: 'Resolve artifact definitions', id: 'lh:config:resolveArtifactsToDefns'};
130
142
  log.time(status, 'verbose');
131
143
 
144
+ const sortedArtifacts = [...artifacts];
145
+ sortedArtifacts.sort((a, b) => {
146
+ const aPriority = internalArtifactPriorities[a.id] || 0;
147
+ const bPriority = internalArtifactPriorities[b.id] || 0;
148
+ return aPriority - bPriority;
149
+ });
150
+
132
151
  /** @type {Map<Symbol, LH.Config.AnyArtifactDefn>} */
133
152
  const artifactDefnsBySymbol = new Map();
134
153
 
135
154
  const coreGathererList = Runner.getGathererList();
136
155
  const artifactDefns = [];
137
- for (const artifactJson of artifacts) {
156
+ for (const artifactJson of sortedArtifacts) {
138
157
  /** @type {LH.Config.GathererJson} */
139
158
  // @ts-expect-error - remove when legacy runner path is removed.
140
159
  const gathererJson = artifactJson.gatherer;
@@ -122,105 +122,57 @@ const UIStrings = {
122
122
 
123
123
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
124
124
 
125
- // Ensure all artifact IDs match the typedefs.
126
- /** @type {Record<keyof LH.FRArtifacts, string>} */
127
- const artifacts = {
128
- DevtoolsLog: '',
129
- Trace: '',
130
- Accessibility: '',
131
- AnchorElements: '',
132
- BFCacheFailures: '',
133
- CacheContents: '',
134
- ConsoleMessages: '',
135
- CSSUsage: '',
136
- Doctype: '',
137
- DOMStats: '',
138
- EmbeddedContent: '',
139
- FontSize: '',
140
- Inputs: '',
141
- FullPageScreenshot: '',
142
- GlobalListeners: '',
143
- IFrameElements: '',
144
- ImageElements: '',
145
- InstallabilityErrors: '',
146
- InspectorIssues: '',
147
- JsUsage: '',
148
- LinkElements: '',
149
- MainDocumentContent: '',
150
- MetaElements: '',
151
- NetworkUserAgent: '',
152
- OptimizedImages: '',
153
- ResponseCompression: '',
154
- RobotsTxt: '',
155
- ServiceWorker: '',
156
- ScriptElements: '',
157
- Scripts: '',
158
- SourceMaps: '',
159
- Stacks: '',
160
- TagsBlockingFirstPaint: '',
161
- TapTargets: '',
162
- TraceElements: '',
163
- ViewportDimensions: '',
164
- WebAppManifest: '',
165
- devtoolsLogs: '',
166
- traces: '',
167
- };
168
-
169
- for (const key of Object.keys(artifacts)) {
170
- artifacts[/** @type {keyof typeof artifacts} */ (key)] = key;
171
- }
172
-
173
125
  /** @type {LH.Config} */
174
126
  const defaultConfig = {
175
127
  settings: constants.defaultSettings,
176
128
  artifacts: [
177
129
  // Artifacts which can be depended on come first.
178
- {id: artifacts.DevtoolsLog, gatherer: 'devtools-log'},
179
- {id: artifacts.Trace, gatherer: 'trace'},
130
+ {id: 'DevtoolsLog', gatherer: 'devtools-log'},
131
+ {id: 'Trace', gatherer: 'trace'},
180
132
 
181
- {id: artifacts.Accessibility, gatherer: 'accessibility'},
182
- {id: artifacts.AnchorElements, gatherer: 'anchor-elements'},
183
- {id: artifacts.CacheContents, gatherer: 'cache-contents'},
184
- {id: artifacts.ConsoleMessages, gatherer: 'console-messages'},
185
- {id: artifacts.CSSUsage, gatherer: 'css-usage'},
186
- {id: artifacts.Doctype, gatherer: 'dobetterweb/doctype'},
187
- {id: artifacts.DOMStats, gatherer: 'dobetterweb/domstats'},
188
- {id: artifacts.EmbeddedContent, gatherer: 'seo/embedded-content'},
189
- {id: artifacts.FontSize, gatherer: 'seo/font-size'},
190
- {id: artifacts.Inputs, gatherer: 'inputs'},
191
- {id: artifacts.GlobalListeners, gatherer: 'global-listeners'},
192
- {id: artifacts.IFrameElements, gatherer: 'iframe-elements'},
193
- {id: artifacts.ImageElements, gatherer: 'image-elements'},
194
- {id: artifacts.InstallabilityErrors, gatherer: 'installability-errors'},
195
- {id: artifacts.InspectorIssues, gatherer: 'inspector-issues'},
196
- {id: artifacts.JsUsage, gatherer: 'js-usage'},
197
- {id: artifacts.LinkElements, gatherer: 'link-elements'},
198
- {id: artifacts.MainDocumentContent, gatherer: 'main-document-content'},
199
- {id: artifacts.MetaElements, gatherer: 'meta-elements'},
200
- {id: artifacts.NetworkUserAgent, gatherer: 'network-user-agent'},
201
- {id: artifacts.OptimizedImages, gatherer: 'dobetterweb/optimized-images'},
202
- {id: artifacts.ResponseCompression, gatherer: 'dobetterweb/response-compression'},
203
- {id: artifacts.RobotsTxt, gatherer: 'seo/robots-txt'},
204
- {id: artifacts.ServiceWorker, gatherer: 'service-worker'},
205
- {id: artifacts.ScriptElements, gatherer: 'script-elements'},
206
- {id: artifacts.Scripts, gatherer: 'scripts'},
207
- {id: artifacts.SourceMaps, gatherer: 'source-maps'},
208
- {id: artifacts.Stacks, gatherer: 'stacks'},
209
- {id: artifacts.TagsBlockingFirstPaint, gatherer: 'dobetterweb/tags-blocking-first-paint'},
210
- {id: artifacts.TapTargets, gatherer: 'seo/tap-targets'},
211
- {id: artifacts.TraceElements, gatherer: 'trace-elements'},
212
- {id: artifacts.ViewportDimensions, gatherer: 'viewport-dimensions'},
213
- {id: artifacts.WebAppManifest, gatherer: 'web-app-manifest'},
133
+ {id: 'Accessibility', gatherer: 'accessibility'},
134
+ {id: 'AnchorElements', gatherer: 'anchor-elements'},
135
+ {id: 'CacheContents', gatherer: 'cache-contents'},
136
+ {id: 'ConsoleMessages', gatherer: 'console-messages'},
137
+ {id: 'CSSUsage', gatherer: 'css-usage'},
138
+ {id: 'Doctype', gatherer: 'dobetterweb/doctype'},
139
+ {id: 'DOMStats', gatherer: 'dobetterweb/domstats'},
140
+ {id: 'EmbeddedContent', gatherer: 'seo/embedded-content'},
141
+ {id: 'FontSize', gatherer: 'seo/font-size'},
142
+ {id: 'Inputs', gatherer: 'inputs'},
143
+ {id: 'GlobalListeners', gatherer: 'global-listeners'},
144
+ {id: 'IFrameElements', gatherer: 'iframe-elements'},
145
+ {id: 'ImageElements', gatherer: 'image-elements'},
146
+ {id: 'InstallabilityErrors', gatherer: 'installability-errors'},
147
+ {id: 'InspectorIssues', gatherer: 'inspector-issues'},
148
+ {id: 'JsUsage', gatherer: 'js-usage'},
149
+ {id: 'LinkElements', gatherer: 'link-elements'},
150
+ {id: 'MainDocumentContent', gatherer: 'main-document-content'},
151
+ {id: 'MetaElements', gatherer: 'meta-elements'},
152
+ {id: 'NetworkUserAgent', gatherer: 'network-user-agent'},
153
+ {id: 'OptimizedImages', gatherer: 'dobetterweb/optimized-images'},
154
+ {id: 'ResponseCompression', gatherer: 'dobetterweb/response-compression'},
155
+ {id: 'RobotsTxt', gatherer: 'seo/robots-txt'},
156
+ {id: 'ServiceWorker', gatherer: 'service-worker'},
157
+ {id: 'ScriptElements', gatherer: 'script-elements'},
158
+ {id: 'Scripts', gatherer: 'scripts'},
159
+ {id: 'SourceMaps', gatherer: 'source-maps'},
160
+ {id: 'Stacks', gatherer: 'stacks'},
161
+ {id: 'TagsBlockingFirstPaint', gatherer: 'dobetterweb/tags-blocking-first-paint'},
162
+ {id: 'TapTargets', gatherer: 'seo/tap-targets'},
163
+ {id: 'TraceElements', gatherer: 'trace-elements'},
164
+ {id: 'ViewportDimensions', gatherer: 'viewport-dimensions'},
165
+ {id: 'WebAppManifest', gatherer: 'web-app-manifest'},
214
166
 
215
167
  // Artifact copies are renamed for compatibility with legacy artifacts.
216
- {id: artifacts.devtoolsLogs, gatherer: 'devtools-log-compat'},
217
- {id: artifacts.traces, gatherer: 'trace-compat'},
168
+ {id: 'devtoolsLogs', gatherer: 'devtools-log-compat'},
169
+ {id: 'traces', gatherer: 'trace-compat'},
218
170
 
219
171
  // FullPageScreenshot comes at the end so all other node analysis is captured.
220
- {id: artifacts.FullPageScreenshot, gatherer: 'full-page-screenshot'},
172
+ {id: 'FullPageScreenshot', gatherer: 'full-page-screenshot'},
221
173
 
222
- // BFCacheErrors comes at the very end because it can perform a page navigation.
223
- {id: artifacts.BFCacheFailures, gatherer: 'bf-cache-failures'},
174
+ // BFCacheFailures comes at the very end because it can perform a page navigation.
175
+ {id: 'BFCacheFailures', gatherer: 'bf-cache-failures'},
224
176
  ],
225
177
  audits: [
226
178
  'is-on-https',
@@ -275,7 +227,7 @@ const defaultConfig = {
275
227
  'non-composited-animations',
276
228
  'unsized-images',
277
229
  'valid-source-maps',
278
- 'preload-lcp-image',
230
+ 'prioritize-lcp-image',
279
231
  'csp-xss',
280
232
  'script-treemap-data',
281
233
  'manual/pwa-cross-browser',
@@ -494,7 +446,7 @@ const defaultConfig = {
494
446
  {id: 'efficient-animated-content', weight: 0},
495
447
  {id: 'duplicated-javascript', weight: 0},
496
448
  {id: 'legacy-javascript', weight: 0},
497
- {id: 'preload-lcp-image', weight: 0},
449
+ {id: 'prioritize-lcp-image', weight: 0},
498
450
  {id: 'total-byte-weight', weight: 0},
499
451
  {id: 'uses-long-cache-ttl', weight: 0},
500
452
  {id: 'dom-size', weight: 0},
@@ -22,7 +22,7 @@ const fcpRelevantAudits = [
22
22
  const lcpRelevantAudits = [
23
23
  ...fcpRelevantAudits,
24
24
  'largest-contentful-paint-element',
25
- 'preload-lcp-image',
25
+ 'prioritize-lcp-image',
26
26
  'unused-javascript',
27
27
  'efficient-animated-content',
28
28
  'total-byte-weight',
@@ -222,9 +222,13 @@ class FullPageScreenshot extends FRGatherer {
222
222
  await this._resizeViewport(context, deviceMetrics);
223
223
  }
224
224
 
225
+ // Issue both commands at once, to reduce the chance that the page changes between capturing
226
+ // a screenshot and resolving the nodes. https://github.com/GoogleChrome/lighthouse/pull/14763
227
+ const [screenshot, nodes] =
228
+ await Promise.all([this._takeScreenshot(context), this._resolveNodes(context)]);
225
229
  return {
226
- screenshot: await this._takeScreenshot(context),
227
- nodes: await this._resolveNodes(context),
230
+ screenshot,
231
+ nodes,
228
232
  };
229
233
  } finally {
230
234
  if (!settings.usePassiveGathering) {
@@ -230,8 +230,9 @@ class TraceElements extends FRGatherer {
230
230
  throw err;
231
231
  }
232
232
 
233
+ // Use main-frame-only LCP to match the metric value.
234
+ const lcpData = processedNavigation.largestContentfulPaintEvt?.args?.data;
233
235
  // These should exist, but trace types are loose.
234
- const lcpData = processedNavigation.largestContentfulPaintAllFramesEvt?.args?.data;
235
236
  if (lcpData?.nodeId === undefined || !lcpData.type) return;
236
237
 
237
238
  return {
@@ -66,12 +66,14 @@ export function _navigations(args: {
66
66
  artifacts: Partial<LH.FRArtifacts & LH.FRBaseArtifacts>;
67
67
  }>;
68
68
  /**
69
- * @param {{requestedUrl?: string, driver: Driver, resolvedConfig: LH.Config.ResolvedConfig}} args
69
+ * @param {{requestedUrl?: string, driver: Driver, resolvedConfig: LH.Config.ResolvedConfig, lhBrowser?: LH.Puppeteer.Browser, lhPage?: LH.Puppeteer.Page}} args
70
70
  */
71
- export function _cleanup({ requestedUrl, driver, resolvedConfig }: {
71
+ export function _cleanup({ requestedUrl, driver, resolvedConfig, lhBrowser, lhPage }: {
72
72
  requestedUrl?: string;
73
73
  driver: Driver;
74
74
  resolvedConfig: LH.Config.ResolvedConfig;
75
+ lhBrowser?: LH.Puppeteer.Browser;
76
+ lhPage?: LH.Puppeteer.Page;
75
77
  }): Promise<void>;
76
78
  import { Driver } from "./driver.js";
77
79
  import { collectPhaseArtifacts } from "./runner-helpers.js";
@@ -309,13 +309,17 @@ async function _navigations(args) {
309
309
  }
310
310
 
311
311
  /**
312
- * @param {{requestedUrl?: string, driver: Driver, resolvedConfig: LH.Config.ResolvedConfig}} args
312
+ * @param {{requestedUrl?: string, driver: Driver, resolvedConfig: LH.Config.ResolvedConfig, lhBrowser?: LH.Puppeteer.Browser, lhPage?: LH.Puppeteer.Page}} args
313
313
  */
314
- async function _cleanup({requestedUrl, driver, resolvedConfig}) {
314
+ async function _cleanup({requestedUrl, driver, resolvedConfig, lhBrowser, lhPage}) {
315
315
  const didResetStorage = !resolvedConfig.settings.disableStorageReset && requestedUrl;
316
316
  if (didResetStorage) await storage.clearDataForOrigin(driver.defaultSession, requestedUrl);
317
317
 
318
318
  await driver.disconnect();
319
+
320
+ // If Lighthouse started the Puppeteer instance then we are responsible for closing it.
321
+ await lhPage?.close();
322
+ await lhBrowser?.disconnect();
319
323
  }
320
324
 
321
325
  /**
@@ -338,17 +342,25 @@ async function navigationGather(page, requestor, options = {}) {
338
342
  async () => {
339
343
  const normalizedRequestor = isCallback ? requestor : UrlUtils.normalizeUrl(requestor);
340
344
 
345
+ /** @type {LH.Puppeteer.Browser|undefined} */
346
+ let lhBrowser = undefined;
347
+ /** @type {LH.Puppeteer.Page|undefined} */
348
+ let lhPage = undefined;
349
+
341
350
  // For navigation mode, we shouldn't connect to a browser in audit mode,
342
351
  // therefore we connect to the browser in the gatherFn callback.
343
352
  if (!page) {
344
353
  const {hostname = DEFAULT_HOSTNAME, port = DEFAULT_PORT} = flags;
345
- const browser = await puppeteer.connect({browserURL: `http://${hostname}:${port}`});
346
- page = await browser.newPage();
354
+ lhBrowser = await puppeteer.connect({browserURL: `http://${hostname}:${port}`});
355
+ lhPage = await lhBrowser.newPage();
356
+ page = lhPage;
347
357
  }
348
358
 
349
359
  const driver = new Driver(page);
350
360
  const context = {
351
361
  driver,
362
+ lhBrowser,
363
+ lhPage,
352
364
  resolvedConfig,
353
365
  requestor: normalizedRequestor,
354
366
  };