lighthouse 9.5.0 → 9.6.3

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 (69) hide show
  1. package/dist/report/bundle.esm.js +67 -384
  2. package/dist/report/flow.js +20 -25
  3. package/dist/report/standalone.js +18 -23
  4. package/lighthouse-cli/test/smokehouse/core-tests.js +2 -0
  5. package/lighthouse-cli/test/smokehouse/frontends/lib.js +1 -3
  6. package/lighthouse-cli/test/smokehouse/frontends/smokehouse-bin.js +1 -3
  7. package/lighthouse-cli/test/smokehouse/lighthouse-runners/cli.js +1 -0
  8. package/lighthouse-cli/test/smokehouse/readme.md +5 -4
  9. package/lighthouse-cli/test/smokehouse/report-assert.js +16 -13
  10. package/lighthouse-cli/test/smokehouse/version-check-test.js +46 -0
  11. package/lighthouse-cli/test/smokehouse/version-check.js +49 -0
  12. package/lighthouse-core/audits/bootup-time.js +2 -70
  13. package/lighthouse-core/audits/byte-efficiency/duplicated-javascript.js +3 -5
  14. package/lighthouse-core/audits/byte-efficiency/legacy-javascript.js +2 -5
  15. package/lighthouse-core/audits/byte-efficiency/unminified-javascript.js +4 -2
  16. package/lighthouse-core/audits/byte-efficiency/unused-javascript.js +3 -1
  17. package/lighthouse-core/audits/deprecations.js +603 -28
  18. package/lighthouse-core/audits/installable-manifest.js +27 -6
  19. package/lighthouse-core/audits/long-tasks.js +3 -3
  20. package/lighthouse-core/audits/metrics/experimental-interaction-to-next-paint.js +88 -0
  21. package/lighthouse-core/audits/network-requests.js +79 -62
  22. package/lighthouse-core/audits/preload-lcp-image.js +33 -7
  23. package/lighthouse-core/audits/third-party-summary.js +3 -3
  24. package/lighthouse-core/audits/work-during-interaction.js +280 -0
  25. package/lighthouse-core/computed/metrics/responsiveness.js +157 -0
  26. package/lighthouse-core/config/config-helpers.js +1 -1
  27. package/lighthouse-core/config/metrics-to-audits.js +5 -0
  28. package/lighthouse-core/fraggle-rock/config/default-config.js +9 -1
  29. package/lighthouse-core/fraggle-rock/gather/base-artifacts.js +2 -2
  30. package/lighthouse-core/fraggle-rock/gather/session.js +8 -2
  31. package/lighthouse-core/gather/driver.js +21 -14
  32. package/lighthouse-core/gather/gather-runner.js +2 -4
  33. package/lighthouse-core/gather/gatherers/css-usage.js +59 -34
  34. package/lighthouse-core/gather/gatherers/trace-elements.js +21 -2
  35. package/lighthouse-core/lib/arbitrary-equality-map.js +2 -2
  36. package/lighthouse-core/lib/dependency-graph/network-node.js +1 -1
  37. package/lighthouse-core/lib/i18n/i18n.js +2 -0
  38. package/lighthouse-core/lib/minify-trace.js +2 -0
  39. package/lighthouse-core/lib/script-helpers.js +24 -0
  40. package/lighthouse-core/lib/stack-packs.js +8 -5
  41. package/lighthouse-core/lib/tracehouse/task-groups.js +1 -0
  42. package/lighthouse-core/lib/tracehouse/task-summary.js +87 -0
  43. package/lighthouse-core/lib/tracehouse/trace-processor.js +39 -14
  44. package/lighthouse-core/runner.js +1 -1
  45. package/lighthouse-core/util-commonjs.js +20 -18
  46. package/package.json +10 -8
  47. package/report/assets/styles.css +5 -2
  48. package/report/clients/bundle.js +1 -0
  49. package/report/renderer/components.js +1 -1
  50. package/report/renderer/details-renderer.js +4 -4
  51. package/report/renderer/performance-category-renderer.js +15 -11
  52. package/report/renderer/util.js +20 -18
  53. package/report/test/renderer/__snapshots__/report-renderer-axe-test.js.snap +6 -98
  54. package/report/test/renderer/details-renderer-test.js +9 -9
  55. package/report/test/renderer/performance-category-renderer-test.js +31 -0
  56. package/report/test/renderer/report-renderer-axe-test.js +3 -15
  57. package/report/test-assets/faux-psi-template.html +3 -2
  58. package/report/test-assets/faux-psi.js +22 -1
  59. package/shared/localization/locales/en-US.json +244 -4
  60. package/shared/localization/locales/en-XL.json +244 -4
  61. package/shared/localization/swap-locale.js +2 -1
  62. package/third-party/chromium-synchronization/installability-errors-test.js +1 -0
  63. package/third-party/snyk/snapshot.json +2 -3
  64. package/tsconfig.json +1 -0
  65. package/types/artifacts.d.ts +40 -1
  66. package/types/lhr/audit-details.d.ts +3 -1
  67. package/types/smokehouse.d.ts +1 -1
  68. package/changelog.md +0 -5902
  69. package/lighthouse-core/scripts/package.json +0 -4
@@ -97,6 +97,11 @@ const UIStrings = {
97
97
  'protocol-timeout': `Lighthouse could not determine if there was a service worker. Please try with a newer version of Chrome.`,
98
98
  /** Message logged when the web app has been uninstalled o desktop, signalling that the install banner state is being reset. */
99
99
  'pipeline-restarted': 'PWA has been uninstalled and installability checks resetting.',
100
+ /**
101
+ * @description Error message explaining that the URL of the manifest uses a scheme that is not supported on Android.
102
+ * @example {data:} scheme
103
+ */
104
+ 'scheme-not-supported-for-webapk': 'The manifest URL scheme ({scheme}) is not supported on Android.',
100
105
  };
101
106
  /* eslint-enable max-len */
102
107
 
@@ -125,7 +130,7 @@ class InstallableManifest extends Audit {
125
130
  failureTitle: str_(UIStrings.failureTitle),
126
131
  description: str_(UIStrings.description),
127
132
  supportedModes: ['navigation'],
128
- requiredArtifacts: ['URL', 'WebAppManifest', 'InstallabilityErrors'],
133
+ requiredArtifacts: ['WebAppManifest', 'InstallabilityErrors'],
129
134
  };
130
135
  }
131
136
 
@@ -156,6 +161,17 @@ class InstallableManifest extends Audit {
156
161
  // @ts-expect-error errorIds from protocol should match up against the strings dict
157
162
  const matchingString = UIStrings[err.errorId];
158
163
 
164
+ if (err.errorId === 'scheme-not-supported-for-webapk') {
165
+ // If there was no manifest, then there will be at lest one other installability error.
166
+ // We can ignore this error if that's the case.
167
+ const manifestUrl = artifacts.WebAppManifest?.url;
168
+ if (!manifestUrl) continue;
169
+
170
+ const scheme = new URL(manifestUrl).protocol;
171
+ i18nErrors.push(str_(matchingString, {scheme}));
172
+ continue;
173
+ }
174
+
159
175
  // Handle an errorId we don't recognize.
160
176
  if (matchingString === undefined) {
161
177
  i18nErrors.push(str_(UIStrings.noErrorId, {errorId: err.errorId}));
@@ -195,7 +211,6 @@ class InstallableManifest extends Audit {
195
211
  *
196
212
  */
197
213
  static async audit(artifacts, context) {
198
- const manifestValues = await ManifestValues.request(artifacts, context);
199
214
  const {i18nErrors, warnings} = InstallableManifest.getInstallabilityErrors(artifacts);
200
215
 
201
216
  const manifestUrl = artifacts.WebAppManifest ? artifacts.WebAppManifest.url : null;
@@ -210,10 +225,16 @@ class InstallableManifest extends Audit {
210
225
  const errorReasons = i18nErrors.map(reason => {
211
226
  return {reason};
212
227
  });
213
- /** DevTools InstallabilityErrors does not emit an error unless there is a manifest, so include manifestValues's error */
214
- if (manifestValues.isParseFailure) {
215
- errorReasons.push({
216
- reason: manifestValues.parseFailureReason});
228
+
229
+ // If InstallabilityErrors is empty, double check ManifestValues to make sure nothing was missed.
230
+ // InstallabilityErrors can be empty erroneously in our DevTools web tests.
231
+ if (!errorReasons.length) {
232
+ const manifestValues = await ManifestValues.request(artifacts, context);
233
+ if (manifestValues.isParseFailure) {
234
+ errorReasons.push({
235
+ reason: manifestValues.parseFailureReason,
236
+ });
237
+ }
217
238
  }
218
239
 
219
240
  // Include the detailed pass/fail checklist as a diagnostic.
@@ -9,9 +9,9 @@ const Audit = require('./audit.js');
9
9
  const NetworkRecords = require('../computed/network-records.js');
10
10
  const i18n = require('../lib/i18n/i18n.js');
11
11
  const MainThreadTasks = require('../computed/main-thread-tasks.js');
12
- const BootupTime = require('./bootup-time.js');
13
12
  const PageDependencyGraph = require('../computed/page-dependency-graph.js');
14
13
  const LoadSimulator = require('../computed/load-simulator.js');
14
+ const {getJavaScriptURLs, getAttributableURLForTask} = require('../lib/tracehouse/task-summary.js');
15
15
 
16
16
  /** We don't always have timing data for short tasks, if we're missing timing data. Treat it as though it were 0ms. */
17
17
  const DEFAULT_TIMING = {startTime: 0, endTime: 0, duration: 0};
@@ -77,7 +77,7 @@ class LongTasks extends Audit {
77
77
  }
78
78
  }
79
79
 
80
- const jsURLs = BootupTime.getJavaScriptURLs(networkRecords);
80
+ const jsURLs = getJavaScriptURLs(networkRecords);
81
81
  // Only consider up to 20 long, top-level (no parent) tasks that have an explicit endTime
82
82
  const longtasks = tasks
83
83
  .map(t => {
@@ -90,7 +90,7 @@ class LongTasks extends Audit {
90
90
 
91
91
  // TODO(beytoven): Add start time that matches with the simulated throttling
92
92
  const results = longtasks.map(task => ({
93
- url: BootupTime.getAttributableURLForTask(task, jsURLs),
93
+ url: getAttributableURLForTask(task, jsURLs),
94
94
  duration: task.duration,
95
95
  startTime: task.startTime,
96
96
  }));
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @license Copyright 2022 The Lighthouse Authors. All Rights Reserved.
3
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
+ */
6
+ 'use strict';
7
+
8
+ const Audit = require('../audit.js');
9
+ const ComputedResponsivenes = require('../../computed/metrics/responsiveness.js');
10
+ const i18n = require('../../lib/i18n/i18n.js');
11
+
12
+ const UIStrings = {
13
+ /** Description of the Interaction to Next Paint metric. This description is displayed within a tooltip when the user hovers on the metric name to see more. No character length limits. 'Learn More' becomes link text to additional documentation. */
14
+ description: 'Interaction to Next Paint measures page responsiveness, how long it ' +
15
+ 'takes the page to visibly respond to user input. [Learn more](https://web.dev/inp/).',
16
+ };
17
+
18
+ const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
19
+
20
+ /**
21
+ * @fileoverview This metric gives a high-percentile measure of responsiveness to input.
22
+ */
23
+ class ExperimentalInteractionToNextPaint extends Audit {
24
+ /**
25
+ * @return {LH.Audit.Meta}
26
+ */
27
+ static get meta() {
28
+ return {
29
+ id: 'experimental-interaction-to-next-paint',
30
+ title: str_(i18n.UIStrings.interactionToNextPaint),
31
+ description: str_(UIStrings.description),
32
+ scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
33
+ supportedModes: ['timespan'],
34
+ requiredArtifacts: ['traces'],
35
+ };
36
+ }
37
+
38
+ /**
39
+ * @return {LH.Audit.ScoreOptions}
40
+ */
41
+ static get defaultOptions() {
42
+ return {
43
+ // https://web.dev/inp/
44
+ // This is using the same threshold as field tools since only supported in
45
+ // unsimulated user flows for now.
46
+ // see https://www.desmos.com/calculator/4xtrhg51th
47
+ p10: 200,
48
+ median: 500,
49
+ };
50
+ }
51
+
52
+ /**
53
+ * @param {LH.Artifacts} artifacts
54
+ * @param {LH.Audit.Context} context
55
+ * @return {Promise<LH.Audit.Product>}
56
+ */
57
+ static async audit(artifacts, context) {
58
+ const {settings} = context;
59
+ // TODO: responsiveness isn't yet supported by lantern.
60
+ if (settings.throttlingMethod === 'simulate') {
61
+ return {score: null, notApplicable: true};
62
+ }
63
+
64
+ const trace = artifacts.traces[Audit.DEFAULT_PASS];
65
+ const metricData = {trace, settings};
66
+ const interactionEvent = await ComputedResponsivenes.request(metricData, context);
67
+
68
+ // TODO: include the no-interaction state in the report instead of using n/a.
69
+ if (interactionEvent === null) {
70
+ return {score: null, notApplicable: true};
71
+ }
72
+
73
+ // TODO: remove workaround once 103.0.5052.0 is sufficiently released.
74
+ const timing = interactionEvent.name === 'FallbackTiming' ?
75
+ interactionEvent.duration : interactionEvent.args.data.duration;
76
+
77
+ return {
78
+ score: Audit.computeLogNormalScore({p10: context.options.p10, median: context.options.median},
79
+ timing),
80
+ numericValue: timing,
81
+ numericUnit: 'millisecond',
82
+ displayValue: str_(i18n.UIStrings.ms, {timeInMs: timing}),
83
+ };
84
+ }
85
+ }
86
+
87
+ module.exports = ExperimentalInteractionToNextPaint;
88
+ module.exports.UIStrings = UIStrings;
@@ -8,6 +8,7 @@
8
8
  const Audit = require('./audit.js');
9
9
  const URL = require('../lib/url-shim.js');
10
10
  const NetworkRecords = require('../computed/network-records.js');
11
+ const MainResource = require('../computed/main-resource.js');
11
12
 
12
13
  class NetworkRequests extends Audit {
13
14
  /**
@@ -19,7 +20,7 @@ class NetworkRequests extends Audit {
19
20
  scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
20
21
  title: 'Network Requests',
21
22
  description: 'Lists the network requests that were made during page load.',
22
- requiredArtifacts: ['devtoolsLogs'],
23
+ requiredArtifacts: ['devtoolsLogs', 'URL', 'GatherContext'],
23
24
  };
24
25
  }
25
26
 
@@ -28,75 +29,91 @@ class NetworkRequests extends Audit {
28
29
  * @param {LH.Audit.Context} context
29
30
  * @return {Promise<LH.Audit.Product>}
30
31
  */
31
- static audit(artifacts, context) {
32
+ static async audit(artifacts, context) {
32
33
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
33
- return NetworkRecords.request(devtoolsLog, context).then(records => {
34
- const earliestStartTime = records.reduce(
35
- (min, record) => Math.min(min, record.startTime),
36
- Infinity
37
- );
34
+ const records = await NetworkRecords.request(devtoolsLog, context);
35
+ const earliestStartTime = records.reduce(
36
+ (min, record) => Math.min(min, record.startTime),
37
+ Infinity
38
+ );
38
39
 
39
- /** @param {number} time */
40
- const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
41
- undefined : (time - earliestStartTime) * 1000;
40
+ // Optional mainFrameId check because the main resource is only available for
41
+ // navigations. TODO: https://github.com/GoogleChrome/lighthouse/issues/14157
42
+ // for the general solution to this.
43
+ /** @type {string|undefined} */
44
+ let mainFrameId;
45
+ if (artifacts.GatherContext.gatherMode === 'navigation') {
46
+ const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
47
+ mainFrameId = mainResource.frameId;
48
+ }
42
49
 
43
- const results = records.map(record => {
44
- const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
45
- const TCPMs = record.lrStatistics?.TCPMs;
46
- const requestMs = record.lrStatistics?.requestMs;
47
- const responseMs = record.lrStatistics?.responseMs;
50
+ /** @param {number} time */
51
+ const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
52
+ undefined : (time - earliestStartTime) * 1000;
48
53
 
49
- return {
50
- url: URL.elideDataURI(record.url),
51
- protocol: record.protocol,
52
- startTime: timeToMs(record.startTime),
53
- endTime: timeToMs(record.endTime),
54
- finished: record.finished,
55
- transferSize: record.transferSize,
56
- resourceSize: record.resourceSize,
57
- statusCode: record.statusCode,
58
- mimeType: record.mimeType,
59
- resourceType: record.resourceType,
60
- lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
61
- lrTCPMs: TCPMs, // Only exists on Lightrider runs
62
- lrRequestMs: requestMs, // Only exists on Lightrider runs
63
- lrResponseMs: responseMs, // Only exists on Lightrider runs
64
- };
65
- });
66
-
67
- // NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
68
- /** @type {LH.Audit.Details.Table['headings']} */
69
- const headings = [
70
- {key: 'url', itemType: 'url', text: 'URL'},
71
- {key: 'protocol', itemType: 'text', text: 'Protocol'},
72
- {key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
73
- {key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
74
- {
75
- key: 'transferSize',
76
- itemType: 'bytes',
77
- displayUnit: 'kb',
78
- granularity: 1,
79
- text: 'Transfer Size',
80
- },
81
- {
82
- key: 'resourceSize',
83
- itemType: 'bytes',
84
- displayUnit: 'kb',
85
- granularity: 1,
86
- text: 'Resource Size',
87
- },
88
- {key: 'statusCode', itemType: 'text', text: 'Status Code'},
89
- {key: 'mimeType', itemType: 'text', text: 'MIME Type'},
90
- {key: 'resourceType', itemType: 'text', text: 'Resource Type'},
91
- ];
92
-
93
- const tableDetails = Audit.makeTableDetails(headings, results);
54
+ const results = records.map(record => {
55
+ const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
56
+ const TCPMs = record.lrStatistics?.TCPMs;
57
+ const requestMs = record.lrStatistics?.requestMs;
58
+ const responseMs = record.lrStatistics?.responseMs;
59
+ // Default these to undefined so omitted from JSON in the negative case.
60
+ const isLinkPreload = record.isLinkPreload || undefined;
61
+ const experimentalFromMainFrame = mainFrameId ?
62
+ ((record.frameId === mainFrameId) || undefined) :
63
+ undefined;
94
64
 
95
65
  return {
96
- score: 1,
97
- details: tableDetails,
66
+ url: URL.elideDataURI(record.url),
67
+ protocol: record.protocol,
68
+ startTime: timeToMs(record.startTime),
69
+ endTime: timeToMs(record.endTime),
70
+ finished: record.finished,
71
+ transferSize: record.transferSize,
72
+ resourceSize: record.resourceSize,
73
+ statusCode: record.statusCode,
74
+ mimeType: record.mimeType,
75
+ resourceType: record.resourceType,
76
+ isLinkPreload,
77
+ experimentalFromMainFrame,
78
+ lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
79
+ lrTCPMs: TCPMs, // Only exists on Lightrider runs
80
+ lrRequestMs: requestMs, // Only exists on Lightrider runs
81
+ lrResponseMs: responseMs, // Only exists on Lightrider runs
98
82
  };
99
83
  });
84
+
85
+ // NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
86
+ /** @type {LH.Audit.Details.Table['headings']} */
87
+ const headings = [
88
+ {key: 'url', itemType: 'url', text: 'URL'},
89
+ {key: 'protocol', itemType: 'text', text: 'Protocol'},
90
+ {key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
91
+ {key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
92
+ {
93
+ key: 'transferSize',
94
+ itemType: 'bytes',
95
+ displayUnit: 'kb',
96
+ granularity: 1,
97
+ text: 'Transfer Size',
98
+ },
99
+ {
100
+ key: 'resourceSize',
101
+ itemType: 'bytes',
102
+ displayUnit: 'kb',
103
+ granularity: 1,
104
+ text: 'Resource Size',
105
+ },
106
+ {key: 'statusCode', itemType: 'text', text: 'Status Code'},
107
+ {key: 'mimeType', itemType: 'text', text: 'MIME Type'},
108
+ {key: 'resourceType', itemType: 'text', text: 'Resource Type'},
109
+ ];
110
+
111
+ const tableDetails = Audit.makeTableDetails(headings, results);
112
+
113
+ return {
114
+ score: 1,
115
+ details: tableDetails,
116
+ };
100
117
  }
101
118
  }
102
119
 
@@ -23,6 +23,10 @@ const UIStrings = {
23
23
 
24
24
  const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
25
25
 
26
+ /**
27
+ * @typedef {Array<{url: string, initiatorType: string}>} InitiatorPath
28
+ */
29
+
26
30
  class PreloadLCPImageAudit extends Audit {
27
31
  /**
28
32
  * @return {LH.Audit.Meta}
@@ -86,22 +90,33 @@ class PreloadLCPImageAudit extends Audit {
86
90
  * @param {LH.Gatherer.Simulation.GraphNode} graph
87
91
  * @param {LH.Artifacts.TraceElement|undefined} lcpElement
88
92
  * @param {Array<LH.Artifacts.ImageElement>} imageElements
89
- * @return {LH.Gatherer.Simulation.GraphNetworkNode|undefined}
93
+ * @return {{lcpNodeToPreload?: LH.Gatherer.Simulation.GraphNetworkNode, initiatorPath?: InitiatorPath}}
90
94
  */
91
95
  static getLCPNodeToPreload(mainResource, graph, lcpElement, imageElements) {
92
- if (!lcpElement) return undefined;
96
+ if (!lcpElement) return {};
93
97
 
94
98
  const lcpImageElement = imageElements.find(elem => {
95
99
  return elem.node.devtoolsNodePath === lcpElement.node.devtoolsNodePath;
96
100
  });
97
101
 
98
- if (!lcpImageElement) return undefined;
102
+ if (!lcpImageElement) return {};
99
103
  const lcpUrl = lcpImageElement.src;
100
104
  const {lcpNode, path} = PreloadLCPImageAudit.findLCPNode(graph, lcpUrl);
101
- if (!lcpNode || !path) return undefined;
105
+ if (!lcpNode || !path) return {};
106
+
102
107
  // eslint-disable-next-line max-len
103
108
  const shouldPreload = PreloadLCPImageAudit.shouldPreloadRequest(lcpNode.record, mainResource, path);
104
- return shouldPreload ? lcpNode : undefined;
109
+ const lcpNodeToPreload = shouldPreload ? lcpNode : undefined;
110
+
111
+ const initiatorPath = [
112
+ {url: lcpNode.record.url, initiatorType: lcpNode.initiatorType},
113
+ ...path.map(n => ({url: n.record.url, initiatorType: n.initiatorType})),
114
+ ];
115
+
116
+ return {
117
+ lcpNodeToPreload,
118
+ initiatorPath,
119
+ };
105
120
  }
106
121
 
107
122
  /**
@@ -215,10 +230,10 @@ class PreloadLCPImageAudit extends Audit {
215
230
 
216
231
  const graph = lanternLCP.pessimisticGraph;
217
232
  // eslint-disable-next-line max-len
218
- const lcpNode = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
233
+ const {lcpNodeToPreload, initiatorPath} = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
219
234
 
220
235
  const {results, wastedMs} =
221
- PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNode, graph, simulator);
236
+ PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
222
237
 
223
238
  /** @type {LH.Audit.Details.Opportunity['headings']} */
224
239
  const headings = [
@@ -228,6 +243,17 @@ class PreloadLCPImageAudit extends Audit {
228
243
  ];
229
244
  const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
230
245
 
246
+ // If LCP element was an image and had valid network records (regardless of
247
+ // if it should be preloaded), it will be found first in the `initiatorPath`.
248
+ // Otherwise path and length will be undefined.
249
+ if (initiatorPath) {
250
+ details.debugData = {
251
+ type: 'debugdata',
252
+ initiatorPath,
253
+ pathLength: initiatorPath.length,
254
+ };
255
+ }
256
+
231
257
  return {
232
258
  score: UnusedBytes.scoreForWastedMs(wastedMs),
233
259
  numericValue: wastedMs,
@@ -6,11 +6,11 @@
6
6
  'use strict';
7
7
 
8
8
  const Audit = require('./audit.js');
9
- const BootupTime = require('./bootup-time.js');
10
9
  const i18n = require('../lib/i18n/i18n.js');
11
10
  const thirdPartyWeb = require('../lib/third-party-web.js');
12
11
  const NetworkRecords = require('../computed/network-records.js');
13
12
  const MainThreadTasks = require('../computed/main-thread-tasks.js');
13
+ const {getJavaScriptURLs, getAttributableURLForTask} = require('../lib/tracehouse/task-summary.js');
14
14
 
15
15
  const UIStrings = {
16
16
  /** 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. */
@@ -98,10 +98,10 @@ class ThirdPartySummary extends Audit {
98
98
  byURL.set(request.url, urlSummary);
99
99
  }
100
100
 
101
- const jsURLs = BootupTime.getJavaScriptURLs(networkRecords);
101
+ const jsURLs = getJavaScriptURLs(networkRecords);
102
102
 
103
103
  for (const task of mainThreadTasks) {
104
- const attributableURL = BootupTime.getAttributableURLForTask(task, jsURLs);
104
+ const attributableURL = getAttributableURLForTask(task, jsURLs);
105
105
 
106
106
  const urlSummary = byURL.get(attributableURL) || {...defaultSummary};
107
107
  const taskDuration = task.selfTime * cpuMultiplier;