lighthouse 10.3.0 → 10.4.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 (113) hide show
  1. package/cli/test/smokehouse/frontends/smokehouse-bin.js +19 -13
  2. package/cli/test/smokehouse/lighthouse-runners/bundle.js +3 -5
  3. package/core/audits/accessibility/aria-dialog-name.d.ts +10 -0
  4. package/core/audits/accessibility/aria-dialog-name.js +45 -0
  5. package/core/audits/accessibility/aria-text.d.ts +10 -0
  6. package/core/audits/accessibility/aria-text.js +44 -0
  7. package/core/audits/accessibility/empty-heading.d.ts +10 -0
  8. package/core/audits/accessibility/empty-heading.js +45 -0
  9. package/core/audits/accessibility/identical-links-same-purpose.d.ts +10 -0
  10. package/core/audits/accessibility/identical-links-same-purpose.js +45 -0
  11. package/core/audits/accessibility/landmark-one-main.d.ts +10 -0
  12. package/core/audits/accessibility/landmark-one-main.js +44 -0
  13. package/core/audits/accessibility/link-in-text-block.d.ts +10 -0
  14. package/core/audits/accessibility/link-in-text-block.js +44 -0
  15. package/core/audits/accessibility/select-name.d.ts +10 -0
  16. package/core/audits/accessibility/select-name.js +44 -0
  17. package/core/audits/accessibility/target-size.d.ts +10 -0
  18. package/core/audits/accessibility/target-size.js +45 -0
  19. package/core/audits/audit.d.ts +6 -0
  20. package/core/audits/audit.js +12 -0
  21. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +22 -6
  22. package/core/audits/byte-efficiency/byte-efficiency-audit.js +97 -25
  23. package/core/audits/long-tasks.d.ts +48 -0
  24. package/core/audits/long-tasks.js +146 -19
  25. package/core/audits/non-composited-animations.js +7 -0
  26. package/core/audits/prioritize-lcp-image.js +2 -1
  27. package/core/audits/redirects.js +4 -0
  28. package/core/audits/seo/link-text.js +10 -0
  29. package/core/audits/unsized-images.js +3 -0
  30. package/core/audits/viewport.js +11 -0
  31. package/core/audits/work-during-interaction.js +13 -2
  32. package/core/computed/js-bundles.js +1 -1
  33. package/core/computed/metrics/tbt-utils.d.ts +26 -0
  34. package/core/computed/metrics/tbt-utils.js +48 -28
  35. package/core/computed/metrics/total-blocking-time.js +1 -1
  36. package/core/computed/tbt-impact-tasks.d.ts +54 -0
  37. package/core/computed/tbt-impact-tasks.js +221 -0
  38. package/core/config/default-config.js +17 -0
  39. package/core/config/filters.d.ts +9 -9
  40. package/core/config/filters.js +7 -7
  41. package/core/config/validation.js +12 -0
  42. package/core/gather/base-gatherer.d.ts +1 -3
  43. package/core/gather/base-gatherer.js +1 -3
  44. package/core/gather/gatherers/accessibility.js +8 -3
  45. package/core/gather/gatherers/seo/font-size.d.ts +0 -1
  46. package/core/gather/gatherers/seo/font-size.js +0 -1
  47. package/core/gather/gatherers/source-maps.js +3 -2
  48. package/core/lib/cdt/SDK.d.ts +1 -1
  49. package/core/lib/cdt/SDK.js +2 -2
  50. package/core/lib/dependency-graph/simulator/simulator.js +3 -1
  51. package/core/lib/lh-error.d.ts +14 -2
  52. package/core/lib/lh-error.js +7 -1
  53. package/core/lib/navigation-error.d.ts +2 -2
  54. package/core/lib/navigation-error.js +1 -1
  55. package/core/lib/network-recorder.js +1 -0
  56. package/core/lib/stack-packs.js +4 -0
  57. package/dist/report/bundle.esm.js +2 -1
  58. package/dist/report/flow.js +9 -9
  59. package/dist/report/standalone.js +2 -2
  60. package/package.json +5 -5
  61. package/report/assets/styles.css +5 -1
  62. package/report/renderer/category-renderer.js +1 -0
  63. package/report/renderer/components.js +1 -1
  64. package/shared/localization/locales/ar-XB.json +233 -2
  65. package/shared/localization/locales/ar.json +233 -2
  66. package/shared/localization/locales/bg.json +233 -2
  67. package/shared/localization/locales/ca.json +233 -2
  68. package/shared/localization/locales/cs.json +246 -15
  69. package/shared/localization/locales/da.json +239 -8
  70. package/shared/localization/locales/de.json +238 -7
  71. package/shared/localization/locales/el.json +234 -3
  72. package/shared/localization/locales/en-GB.json +233 -2
  73. package/shared/localization/locales/en-US.json +87 -0
  74. package/shared/localization/locales/en-XA.json +233 -2
  75. package/shared/localization/locales/en-XL.json +87 -0
  76. package/shared/localization/locales/es-419.json +233 -2
  77. package/shared/localization/locales/es.json +233 -2
  78. package/shared/localization/locales/fi.json +233 -2
  79. package/shared/localization/locales/fil.json +236 -5
  80. package/shared/localization/locales/fr.json +235 -4
  81. package/shared/localization/locales/he.json +233 -2
  82. package/shared/localization/locales/hi.json +236 -5
  83. package/shared/localization/locales/hr.json +234 -3
  84. package/shared/localization/locales/hu.json +233 -2
  85. package/shared/localization/locales/id.json +233 -2
  86. package/shared/localization/locales/it.json +234 -3
  87. package/shared/localization/locales/ja.json +233 -2
  88. package/shared/localization/locales/ko.json +233 -2
  89. package/shared/localization/locales/lt.json +233 -2
  90. package/shared/localization/locales/lv.json +250 -19
  91. package/shared/localization/locales/nl.json +233 -2
  92. package/shared/localization/locales/no.json +235 -4
  93. package/shared/localization/locales/pl.json +233 -2
  94. package/shared/localization/locales/pt-PT.json +233 -2
  95. package/shared/localization/locales/pt.json +234 -3
  96. package/shared/localization/locales/ro.json +233 -2
  97. package/shared/localization/locales/ru.json +233 -2
  98. package/shared/localization/locales/sk.json +233 -2
  99. package/shared/localization/locales/sl.json +233 -2
  100. package/shared/localization/locales/sr-Latn.json +233 -2
  101. package/shared/localization/locales/sr.json +233 -2
  102. package/shared/localization/locales/sv.json +233 -2
  103. package/shared/localization/locales/ta.json +233 -2
  104. package/shared/localization/locales/te.json +233 -2
  105. package/shared/localization/locales/th.json +233 -2
  106. package/shared/localization/locales/tr.json +233 -2
  107. package/shared/localization/locales/uk.json +233 -2
  108. package/shared/localization/locales/vi.json +233 -2
  109. package/shared/localization/locales/zh-HK.json +233 -2
  110. package/shared/localization/locales/zh-TW.json +235 -4
  111. package/shared/localization/locales/zh.json +239 -8
  112. package/tsconfig.json +1 -1
  113. package/types/artifacts.d.ts +9 -2
@@ -10,6 +10,9 @@ import * as i18n from '../../lib/i18n/i18n.js';
10
10
  import {NetworkRecords} from '../../computed/network-records.js';
11
11
  import {LoadSimulator} from '../../computed/load-simulator.js';
12
12
  import {PageDependencyGraph} from '../../computed/page-dependency-graph.js';
13
+ import {LanternLargestContentfulPaint} from '../../computed/metrics/lantern-largest-contentful-paint.js';
14
+ import {LanternFirstContentfulPaint} from '../../computed/metrics/lantern-first-contentful-paint.js';
15
+ import {LCPImageRecord} from '../../computed/lcp-image-record.js';
13
16
 
14
17
  const str_ = i18n.createIcuMessageFn(import.meta.url, {});
15
18
 
@@ -104,9 +107,7 @@ class ByteEfficiencyAudit extends Audit {
104
107
  */
105
108
  static async audit(artifacts, context) {
106
109
  const gatherContext = artifacts.GatherContext;
107
- const trace = artifacts.traces[Audit.DEFAULT_PASS];
108
110
  const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
109
- const URL = artifacts.URL;
110
111
  const settings = context?.settings || {};
111
112
  const simulatorOptions = {
112
113
  devtoolsLog,
@@ -125,34 +126,29 @@ class ByteEfficiencyAudit extends Audit {
125
126
  };
126
127
  }
127
128
 
128
- const [result, graph, simulator] = await Promise.all([
129
+ const metricComputationInput = Audit.makeMetricComputationDataInput(artifacts, context);
130
+
131
+ const [result, simulator] = await Promise.all([
129
132
  this.audit_(artifacts, networkRecords, context),
130
- // Page dependency graph is only used in navigation mode.
131
- gatherContext.gatherMode === 'navigation' ?
132
- PageDependencyGraph.request({trace, devtoolsLog, URL}, context) :
133
- null,
134
133
  LoadSimulator.request(simulatorOptions, context),
135
134
  ]);
136
135
 
137
- return this.createAuditProduct(result, graph, simulator, gatherContext);
136
+ return this.createAuditProduct(result, simulator, metricComputationInput, context);
138
137
  }
139
138
 
140
139
  /**
141
- * Computes the estimated effect of all the byte savings on the maximum of the following:
142
- *
143
- * - end time of the last long task in the provided graph
144
- * - (if includeLoad is true or not provided) end time of the last node in the graph
140
+ * Computes the estimated effect of all the byte savings on the provided graph.
145
141
  *
146
142
  * @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
147
143
  * @param {Node} graph
148
144
  * @param {Simulator} simulator
149
- * @param {{includeLoad?: boolean, label?: string, providedWastedBytesByUrl?: Map<string, number>}=} options
150
- * @return {number}
145
+ * @param {{label?: string, providedWastedBytesByUrl?: Map<string, number>}=} options
146
+ * @return {{savings: number, simulationBeforeChanges: LH.Gatherer.Simulation.Result, simulationAfterChanges: LH.Gatherer.Simulation.Result}}
151
147
  */
152
- static computeWasteWithTTIGraph(results, graph, simulator, options) {
153
- options = Object.assign({includeLoad: true, label: this.meta.id}, options);
154
- const beforeLabel = `${options.label}-before`;
155
- const afterLabel = `${options.label}-after`;
148
+ static computeWasteWithGraph(results, graph, simulator, options) {
149
+ options = Object.assign({label: ''}, options);
150
+ const beforeLabel = `${this.meta.id}-${options.label}-before`;
151
+ const afterLabel = `${this.meta.id}-${options.label}-after`;
156
152
 
157
153
  const simulationBeforeChanges = simulator.simulate(graph, {label: beforeLabel});
158
154
 
@@ -187,7 +183,36 @@ class ByteEfficiencyAudit extends Audit {
187
183
  node.record.transferSize = originalTransferSize;
188
184
  });
189
185
 
190
- const savingsOnOverallLoad = simulationBeforeChanges.timeInMs - simulationAfterChanges.timeInMs;
186
+ const savings = simulationBeforeChanges.timeInMs - simulationAfterChanges.timeInMs;
187
+
188
+ return {
189
+ // Round waste to nearest 10ms
190
+ savings: Math.round(Math.max(savings, 0) / 10) * 10,
191
+ simulationBeforeChanges,
192
+ simulationAfterChanges,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Computes the estimated effect of all the byte savings on the maximum of the following:
198
+ *
199
+ * - end time of the last long task in the provided graph
200
+ * - (if includeLoad is true or not provided) end time of the last node in the graph
201
+ *
202
+ * @param {Array<LH.Audit.ByteEfficiencyItem>} results The array of byte savings results per resource
203
+ * @param {Node} graph
204
+ * @param {Simulator} simulator
205
+ * @param {{includeLoad?: boolean, providedWastedBytesByUrl?: Map<string, number>}=} options
206
+ * @return {number}
207
+ */
208
+ static computeWasteWithTTIGraph(results, graph, simulator, options) {
209
+ options = Object.assign({includeLoad: true}, options);
210
+ const {savings: savingsOnOverallLoad, simulationBeforeChanges, simulationAfterChanges} =
211
+ this.computeWasteWithGraph(results, graph, simulator, {
212
+ ...options,
213
+ label: 'overallLoad',
214
+ });
215
+
191
216
  const savingsOnTTI =
192
217
  LanternInteractive.getLastLongTaskEndTime(simulationBeforeChanges.nodeTimings) -
193
218
  LanternInteractive.getLastLongTaskEndTime(simulationAfterChanges.nodeTimings);
@@ -201,24 +226,63 @@ class ByteEfficiencyAudit extends Audit {
201
226
 
202
227
  /**
203
228
  * @param {ByteEfficiencyProduct} result
204
- * @param {Node|null} graph
205
229
  * @param {Simulator} simulator
206
- * @param {LH.Artifacts['GatherContext']} gatherContext
207
- * @return {LH.Audit.Product}
230
+ * @param {LH.Artifacts.MetricComputationDataInput} metricComputationInput
231
+ * @param {LH.Audit.Context} context
232
+ * @return {Promise<LH.Audit.Product>}
208
233
  */
209
- static createAuditProduct(result, graph, simulator, gatherContext) {
234
+ static async createAuditProduct(result, simulator, metricComputationInput, context) {
210
235
  const results = result.items.sort((itemA, itemB) => itemB.wastedBytes - itemA.wastedBytes);
211
236
 
212
237
  const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
213
238
 
239
+ /** @type {LH.Audit.MetricSavings} */
240
+ const metricSavings = {
241
+ FCP: 0,
242
+ LCP: 0,
243
+ };
244
+
214
245
  // `wastedMs` may be negative, if making the opportunity change could be detrimental.
215
246
  // This is useful information in the LHR and should be preserved.
216
247
  let wastedMs;
217
- if (gatherContext.gatherMode === 'navigation') {
218
- if (!graph) throw Error('Page dependency graph should always be computed in navigation mode');
248
+ if (metricComputationInput.gatherContext.gatherMode === 'navigation') {
249
+ const graph = await PageDependencyGraph.request(metricComputationInput, context);
250
+ const {
251
+ pessimisticGraph: pessimisticFCPGraph,
252
+ } = await LanternFirstContentfulPaint.request(metricComputationInput, context);
253
+ const {
254
+ pessimisticGraph: pessimisticLCPGraph,
255
+ } = await LanternLargestContentfulPaint.request(metricComputationInput, context);
256
+
219
257
  wastedMs = this.computeWasteWithTTIGraph(results, graph, simulator, {
220
258
  providedWastedBytesByUrl: result.wastedBytesByUrl,
221
259
  });
260
+
261
+ const {savings: fcpSavings} = this.computeWasteWithGraph(
262
+ results,
263
+ pessimisticFCPGraph,
264
+ simulator,
265
+ {providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'fcp'}
266
+ );
267
+ const {savings: lcpGraphSavings} = this.computeWasteWithGraph(
268
+ results,
269
+ pessimisticLCPGraph,
270
+ simulator,
271
+ {providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'lcp'}
272
+ );
273
+
274
+ // The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
275
+ let lcpRecordSavings = 0;
276
+ const lcpRecord = await LCPImageRecord.request(metricComputationInput, context);
277
+ if (lcpRecord) {
278
+ const lcpResult = results.find(result => result.url === lcpRecord.url);
279
+ if (lcpResult) {
280
+ lcpRecordSavings = simulator.computeWastedMsFromWastedBytes(lcpResult.wastedBytes);
281
+ }
282
+ }
283
+
284
+ metricSavings.FCP = fcpSavings;
285
+ metricSavings.LCP = Math.max(lcpGraphSavings, lcpRecordSavings);
222
286
  } else {
223
287
  wastedMs = simulator.computeWastedMsFromWastedBytes(wastedBytes);
224
288
  }
@@ -232,6 +296,13 @@ class ByteEfficiencyAudit extends Audit {
232
296
  const details = Audit.makeOpportunityDetails(result.headings, results,
233
297
  {overallSavingsMs: wastedMs, overallSavingsBytes: wastedBytes, sortedBy});
234
298
 
299
+ // TODO: Remove from debug data once `metricSavings` is added to the LHR.
300
+ // For now, add it to debug data for visibility.
301
+ details.debugData = {
302
+ type: 'debugdata',
303
+ metricSavings,
304
+ };
305
+
235
306
  return {
236
307
  explanation: result.explanation,
237
308
  warnings: result.warnings,
@@ -240,6 +311,7 @@ class ByteEfficiencyAudit extends Audit {
240
311
  numericUnit: 'millisecond',
241
312
  score: ByteEfficiencyAudit.scoreForWastedMs(wastedMs),
242
313
  details,
314
+ metricSavings,
243
315
  };
244
316
  }
245
317
 
@@ -1,5 +1,53 @@
1
1
  export default LongTasks;
2
+ export type TaskGroupIds = import('../lib/tracehouse/task-groups.js').TaskGroupIds;
3
+ export type Timing = {
4
+ startTime: number;
5
+ duration: number;
6
+ };
7
+ export type DebugTask = Timing & {
8
+ [p: string]: number;
9
+ urlIndex: number;
10
+ };
11
+ /** @typedef {import('../lib/tracehouse/task-groups.js').TaskGroupIds} TaskGroupIds */
12
+ /** @typedef {{startTime: number, duration: number}} Timing */
13
+ /** @typedef {Timing & {urlIndex: number, [p: string]: number}} DebugTask */
2
14
  declare class LongTasks extends Audit {
15
+ /**
16
+ * Returns the timing information for the given task, recursively walking the
17
+ * task's children and adding up time spent in each type of task activity.
18
+ * If `taskTimingsByEvent` is present, it will be used for task timing instead
19
+ * of the timings on the tasks themselves.
20
+ * If `timeByTaskGroup` is not provided, a new Map will be populated with
21
+ * timing breakdown; if one is provided, timing breakdown will be added to the
22
+ * existing breakdown.
23
+ *
24
+ * TODO: when simulated, a significant number of child tasks are dropped, so
25
+ * most time will be attributed to 'other' (the category of the top-level
26
+ * RunTask). See pruning in `PageDependencyGraph.linkCPUNodes`.
27
+ * @param {LH.Artifacts.TaskNode} task
28
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
29
+ * @param {Map<TaskGroupIds, number>} [timeByTaskGroup]
30
+ * @return {{startTime: number, duration: number, timeByTaskGroup: Map<TaskGroupIds, number>}}
31
+ */
32
+ static getTimingBreakdown(task: LH.Artifacts.TaskNode, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined, timeByTaskGroup?: Map<import("../lib/tracehouse/task-groups.js").TaskGroupIds, number> | undefined): {
33
+ startTime: number;
34
+ duration: number;
35
+ timeByTaskGroup: Map<TaskGroupIds, number>;
36
+ };
37
+ /**
38
+ * @param {Array<LH.Artifacts.TaskNode>} longTasks
39
+ * @param {Set<string>} jsUrls
40
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
41
+ * @return {LH.Audit.Details.DebugData}
42
+ */
43
+ static makeDebugData(longTasks: Array<LH.Artifacts.TaskNode>, jsUrls: Set<string>, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined): LH.Audit.Details.DebugData;
44
+ /**
45
+ * Get timing from task, overridden by taskTimingsByEvent if provided.
46
+ * @param {LH.Artifacts.TaskNode} task
47
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
48
+ * @return {Timing}
49
+ */
50
+ static getTiming(task: LH.Artifacts.TaskNode, taskTimingsByEvent: Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming> | undefined): Timing;
3
51
  /**
4
52
  * @param {LH.Artifacts} artifacts
5
53
  * @param {LH.Audit.Context} context
@@ -14,6 +14,7 @@ import {getJavaScriptURLs, getAttributableURLForTask} from '../lib/tracehouse/ta
14
14
 
15
15
  /** We don't always have timing data for short tasks, if we're missing timing data. Treat it as though it were 0ms. */
16
16
  const DEFAULT_TIMING = {startTime: 0, endTime: 0, duration: 0};
17
+ const DISPLAYED_TASK_COUNT = 20;
17
18
 
18
19
  const UIStrings = {
19
20
  /** Title of a diagnostic LH audit that provides details on the longest running tasks that occur when the page loads. */
@@ -31,6 +32,30 @@ const UIStrings = {
31
32
 
32
33
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
33
34
 
35
+ /**
36
+ * Insert `url` into `urls` array if not already present. Returns
37
+ * the index of `url` in `urls` for later lookup.
38
+ * @param {Array<string>} urls
39
+ * @param {string} url
40
+ */
41
+ function insertUrl(urls, url) {
42
+ const index = urls.indexOf(url);
43
+ if (index > -1) return index;
44
+ return urls.push(url) - 1;
45
+ }
46
+
47
+ /**
48
+ * @param {number} value
49
+ * @return {number}
50
+ */
51
+ function roundTenths(value) {
52
+ return Math.round(value * 10) / 10;
53
+ }
54
+
55
+ /** @typedef {import('../lib/tracehouse/task-groups.js').TaskGroupIds} TaskGroupIds */
56
+ /** @typedef {{startTime: number, duration: number}} Timing */
57
+ /** @typedef {Timing & {urlIndex: number, [p: string]: number}} DebugTask */
58
+
34
59
  class LongTasks extends Audit {
35
60
  /**
36
61
  * @return {LH.Audit.Meta}
@@ -45,6 +70,102 @@ class LongTasks extends Audit {
45
70
  };
46
71
  }
47
72
 
73
+ /**
74
+ * Returns the timing information for the given task, recursively walking the
75
+ * task's children and adding up time spent in each type of task activity.
76
+ * If `taskTimingsByEvent` is present, it will be used for task timing instead
77
+ * of the timings on the tasks themselves.
78
+ * If `timeByTaskGroup` is not provided, a new Map will be populated with
79
+ * timing breakdown; if one is provided, timing breakdown will be added to the
80
+ * existing breakdown.
81
+ *
82
+ * TODO: when simulated, a significant number of child tasks are dropped, so
83
+ * most time will be attributed to 'other' (the category of the top-level
84
+ * RunTask). See pruning in `PageDependencyGraph.linkCPUNodes`.
85
+ * @param {LH.Artifacts.TaskNode} task
86
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
87
+ * @param {Map<TaskGroupIds, number>} [timeByTaskGroup]
88
+ * @return {{startTime: number, duration: number, timeByTaskGroup: Map<TaskGroupIds, number>}}
89
+ */
90
+ static getTimingBreakdown(task, taskTimingsByEvent, timeByTaskGroup = new Map()) {
91
+ const taskTiming = LongTasks.getTiming(task, taskTimingsByEvent);
92
+
93
+ // Add up child time, while recursively stepping in to accumulate group times.
94
+ let childrenTime = 0;
95
+ if (taskTiming.duration > 0) {
96
+ for (const child of task.children) {
97
+ const {duration} = LongTasks.getTimingBreakdown(child, taskTimingsByEvent, timeByTaskGroup);
98
+ childrenTime += duration;
99
+ }
100
+ }
101
+
102
+ // Add this task's selfTime to its group's total time.
103
+ const selfTime = taskTiming.duration - childrenTime;
104
+ const taskGroupTime = timeByTaskGroup.get(task.group.id) || 0;
105
+ timeByTaskGroup.set(task.group.id, taskGroupTime + selfTime);
106
+
107
+ return {
108
+ startTime: taskTiming.startTime,
109
+ duration: taskTiming.duration,
110
+ timeByTaskGroup,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * @param {Array<LH.Artifacts.TaskNode>} longTasks
116
+ * @param {Set<string>} jsUrls
117
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
118
+ * @return {LH.Audit.Details.DebugData}
119
+ */
120
+ static makeDebugData(longTasks, jsUrls, taskTimingsByEvent) {
121
+ /** @type {Array<string>} */
122
+ const urls = [];
123
+ /** @type {Array<DebugTask>} */
124
+ const tasks = [];
125
+
126
+ for (const longTask of longTasks) {
127
+ const attributableUrl = getAttributableURLForTask(longTask, jsUrls);
128
+
129
+ const {startTime, duration, timeByTaskGroup} =
130
+ LongTasks.getTimingBreakdown(longTask, taskTimingsByEvent);
131
+
132
+ // Round time per group and sort entries so order is consistent.
133
+ const timeByTaskGroupEntries = [...timeByTaskGroup]
134
+ .map(/** @return {[TaskGroupIds, number]} */ ([group, time]) => [group, roundTenths(time)])
135
+ .sort((a, b) => a[0].localeCompare(b[0]));
136
+
137
+ tasks.push({
138
+ urlIndex: insertUrl(urls, attributableUrl),
139
+ startTime: roundTenths(startTime),
140
+ duration: roundTenths(duration),
141
+ ...Object.fromEntries(timeByTaskGroupEntries),
142
+ });
143
+ }
144
+
145
+ return {
146
+ type: 'debugdata',
147
+ urls,
148
+ tasks,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Get timing from task, overridden by taskTimingsByEvent if provided.
154
+ * @param {LH.Artifacts.TaskNode} task
155
+ * @param {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} taskTimingsByEvent
156
+ * @return {Timing}
157
+ */
158
+ static getTiming(task, taskTimingsByEvent) {
159
+ /** @type {Timing} */
160
+ let timing = task;
161
+ if (taskTimingsByEvent) {
162
+ timing = taskTimingsByEvent.get(task.event) || DEFAULT_TIMING;
163
+ }
164
+
165
+ const {duration, startTime} = timing;
166
+ return {duration, startTime};
167
+ }
168
+
48
169
  /**
49
170
  * @param {LH.Artifacts} artifacts
50
171
  * @param {LH.Audit.Context} context
@@ -58,10 +179,12 @@ class LongTasks extends Audit {
58
179
  const devtoolsLog = artifacts.devtoolsLogs[LongTasks.DEFAULT_PASS];
59
180
  const networkRecords = await NetworkRecords.request(devtoolsLog, context);
60
181
 
61
- /** @type {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>} */
62
- const taskTimingsByEvent = new Map();
182
+ /** @type {Map<LH.TraceEvent, LH.Gatherer.Simulation.NodeTiming>|undefined} */
183
+ let taskTimingsByEvent;
63
184
 
64
185
  if (settings.throttlingMethod === 'simulate') {
186
+ taskTimingsByEvent = new Map();
187
+
65
188
  const simulatorOptions = {devtoolsLog, settings: context.settings};
66
189
  const pageGraph = await PageDependencyGraph.request({trace, devtoolsLog, URL}, context);
67
190
  const simulator = await LoadSimulator.request(simulatorOptions, context);
@@ -70,30 +193,32 @@ class LongTasks extends Audit {
70
193
  if (node.type !== 'cpu') continue;
71
194
  taskTimingsByEvent.set(node.event, timing);
72
195
  }
73
- } else {
74
- for (const task of tasks) {
75
- if (task.unbounded || task.parent) continue;
76
- taskTimingsByEvent.set(task.event, task);
77
- }
78
196
  }
79
197
 
80
198
  const jsURLs = getJavaScriptURLs(networkRecords);
81
- // Only consider up to 20 long, top-level (no parent) tasks that have an explicit endTime
82
- const longtasks = tasks
83
- .map(t => {
84
- const timing = taskTimingsByEvent.get(t.event) || DEFAULT_TIMING;
85
- return {...t, duration: timing.duration, startTime: timing.startTime};
199
+
200
+ // Only consider top-level (no parent) long tasks that have an explicit endTime.
201
+ const longTasks = tasks
202
+ .map(task => {
203
+ // Use duration from simulation, if available.
204
+ const {duration} = LongTasks.getTiming(task, taskTimingsByEvent);
205
+ return {task, duration};
206
+ })
207
+ .filter(({task, duration}) => {
208
+ return duration >= 50 && !task.unbounded && !task.parent;
86
209
  })
87
- .filter(t => t.duration >= 50 && !t.unbounded && !t.parent)
88
210
  .sort((a, b) => b.duration - a.duration)
89
- .slice(0, 20);
211
+ .map(({task}) => task);
90
212
 
91
213
  // TODO(beytoven): Add start time that matches with the simulated throttling
92
- const results = longtasks.map(task => ({
93
- url: getAttributableURLForTask(task, jsURLs),
94
- duration: task.duration,
95
- startTime: task.startTime,
96
- }));
214
+ const results = longTasks.map(task => {
215
+ const timing = LongTasks.getTiming(task, taskTimingsByEvent);
216
+ return {
217
+ url: getAttributableURLForTask(task, jsURLs),
218
+ duration: timing.duration,
219
+ startTime: timing.startTime,
220
+ };
221
+ }).slice(0, DISPLAYED_TASK_COUNT);
97
222
 
98
223
  /** @type {LH.Audit.Details.Table['headings']} */
99
224
  const headings = [
@@ -107,6 +232,8 @@ class LongTasks extends Audit {
107
232
  const tableDetails = Audit.makeTableDetails(headings, results,
108
233
  {sortedBy: ['duration'], skipSumming: ['startTime']});
109
234
 
235
+ tableDetails.debugData = LongTasks.makeDebugData(longTasks, jsURLs, taskTimingsByEvent);
236
+
110
237
  let displayValue;
111
238
  if (results.length > 0) {
112
239
  displayValue = str_(UIStrings.displayValue, {itemCount: results.length});
@@ -125,6 +125,7 @@ class NonCompositedAnimations extends Audit {
125
125
  return {
126
126
  score: 1,
127
127
  notApplicable: true,
128
+ metricSavings: {CLS: 0},
128
129
  };
129
130
  }
130
131
 
@@ -195,6 +196,12 @@ class NonCompositedAnimations extends Audit {
195
196
  return {
196
197
  score: results.length === 0 ? 1 : 0,
197
198
  notApplicable: results.length === 0,
199
+ metricSavings: {
200
+ // We do not have enough information to accurately predict the impact of individual animations on CLS.
201
+ // It is also not worth the effort since only a small percentage of sites have their CLS affected by non-composited animations.
202
+ // https://github.com/GoogleChrome/lighthouse/pull/15099#issuecomment-1558107906
203
+ CLS: 0,
204
+ },
198
205
  details,
199
206
  displayValue,
200
207
  };
@@ -244,7 +244,7 @@ class PrioritizeLcpImage extends Audit {
244
244
  .find(element => element.traceEventType === 'largest-contentful-paint');
245
245
 
246
246
  if (!lcpElement || lcpElement.type !== 'image') {
247
- return {score: null, notApplicable: true};
247
+ return {score: null, notApplicable: true, metricSavings: {LCP: 0}};
248
248
  }
249
249
 
250
250
  const mainResource = await MainResource.request({devtoolsLog, URL}, context);
@@ -286,6 +286,7 @@ class PrioritizeLcpImage extends Audit {
286
286
  numericUnit: 'millisecond',
287
287
  displayValue: wastedMs ? str_(i18n.UIStrings.displayValueMsSavings, {wastedMs}) : '',
288
288
  details,
289
+ metricSavings: {LCP: wastedMs},
289
290
  };
290
291
  }
291
292
  }
@@ -154,6 +154,10 @@ class Redirects extends Audit {
154
154
  str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: totalWastedMs}) :
155
155
  '',
156
156
  details,
157
+ metricSavings: {
158
+ LCP: totalWastedMs,
159
+ FCP: totalWastedMs,
160
+ },
157
161
  };
158
162
  }
159
163
  }
@@ -74,6 +74,16 @@ const BLOCKLIST = new Set([
74
74
  'mer',
75
75
  'mer info',
76
76
  'mer information',
77
+ // Tamil
78
+ 'அடுத்த பக்கம்',
79
+ 'மறுபக்கம்',
80
+ 'முந்தைய பக்கம்',
81
+ 'முன்பக்கம்',
82
+ 'மேலும் அறிக',
83
+ 'மேலும் தகவலுக்கு',
84
+ 'மேலும் தரவுகளுக்கு',
85
+ 'தயவுசெய்து இங்கே அழுத்தவும்',
86
+ 'இங்கே கிளிக் செய்யவும்',
77
87
  ]);
78
88
 
79
89
  const UIStrings = {
@@ -154,6 +154,9 @@ class UnsizedImages extends Audit {
154
154
  score: unsizedImages.length > 0 ? 0 : 1,
155
155
  notApplicable: images.length === 0,
156
156
  details: Audit.makeTableDetails(headings, unsizedImages),
157
+ metricSavings: {
158
+ CLS: 0,
159
+ },
157
160
  };
158
161
  }
159
162
  }
@@ -46,15 +46,26 @@ class Viewport extends Audit {
46
46
  static async audit(artifacts, context) {
47
47
  const viewportMeta = await ViewportMeta.request(artifacts.MetaElements, context);
48
48
 
49
+ let inpSavings = 300;
49
50
  if (!viewportMeta.hasViewportTag) {
50
51
  return {
51
52
  score: 0,
52
53
  explanation: str_(UIStrings.explanationNoTag),
54
+ metricSavings: {
55
+ INP: inpSavings,
56
+ },
53
57
  };
54
58
  }
55
59
 
60
+ if (viewportMeta.isMobileOptimized) {
61
+ inpSavings = 0;
62
+ }
63
+
56
64
  return {
57
65
  score: Number(viewportMeta.isMobileOptimized),
66
+ metricSavings: {
67
+ INP: inpSavings,
68
+ },
58
69
  warnings: viewportMeta.parserWarnings,
59
70
  };
60
71
  }
@@ -224,7 +224,11 @@ class WorkDuringInteraction extends Audit {
224
224
  const {settings} = context;
225
225
  // TODO: responsiveness isn't yet supported by lantern.
226
226
  if (settings.throttlingMethod === 'simulate') {
227
- return {score: null, notApplicable: true};
227
+ return {
228
+ score: null,
229
+ notApplicable: true,
230
+ metricSavings: {INP: 0},
231
+ };
228
232
  }
229
233
 
230
234
  const trace = artifacts.traces[WorkDuringInteraction.DEFAULT_PASS];
@@ -232,7 +236,11 @@ class WorkDuringInteraction extends Audit {
232
236
  const interactionEvent = await Responsiveness.request(metricData, context);
233
237
  // If no interaction, diagnostic audit is n/a.
234
238
  if (interactionEvent === null) {
235
- return {score: null, notApplicable: true};
239
+ return {
240
+ score: null,
241
+ notApplicable: true,
242
+ metricSavings: {INP: 0},
243
+ };
236
244
  }
237
245
  // TODO: remove workaround once 103.0.5052.0 is sufficiently released.
238
246
  if (interactionEvent.name === 'FallbackTiming') {
@@ -271,6 +279,9 @@ class WorkDuringInteraction extends Audit {
271
279
  type: 'list',
272
280
  items: auditDetailsItems,
273
281
  },
282
+ metricSavings: {
283
+ INP: duration,
284
+ },
274
285
  };
275
286
  }
276
287
  }
@@ -99,7 +99,7 @@ class JSBundles {
99
99
 
100
100
  const compiledUrl = SourceMap.scriptUrl || 'compiled.js';
101
101
  const mapUrl = SourceMap.sourceMapUrl || 'compiled.js.map';
102
- const map = new SDK.TextSourceMap(compiledUrl, mapUrl, rawMap);
102
+ const map = new SDK.SourceMap(compiledUrl, mapUrl, rawMap);
103
103
 
104
104
  const sizes = computeGeneratedFileSizes(map, script.length || 0, script.content || '');
105
105
 
@@ -15,4 +15,30 @@ export function calculateSumOfBlockingTime(topLevelEvents: Array<{
15
15
  end: number;
16
16
  duration: number;
17
17
  }>, startTimeMs: number, endTimeMs: number): number;
18
+ /**
19
+ * For TBT, We only want to consider tasks that fall in our time range
20
+ * - FCP and TTI for navigation mode
21
+ * - Trace start and trace end for timespan mode
22
+ *
23
+ * FCP is picked as `startTimeMs` because there is little risk of user input happening
24
+ * before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
25
+ * optimizing to reach FCP as fast as possible without having to worry about task lengths.
26
+ *
27
+ * TTI is picked as `endTimeMs` because we want a well defined end point for page load.
28
+ *
29
+ * @param {{start: number, end: number, duration: number}} event
30
+ * @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
31
+ * @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
32
+ * @param {{start: number, end: number, duration: number}} [topLevelEvent] Leave unset if `event` is top level. Has no effect if `event` has the same duration as `topLevelEvent`.
33
+ * @return {number}
34
+ */
35
+ export function calculateTbtImpactForEvent(event: {
36
+ start: number;
37
+ end: number;
38
+ duration: number;
39
+ }, startTimeMs: number, endTimeMs: number, topLevelEvent?: {
40
+ start: number;
41
+ end: number;
42
+ duration: number;
43
+ } | undefined): number;
18
44
  //# sourceMappingURL=tbt-utils.d.ts.map