lighthouse 10.3.0 → 10.4.0-dev.20230711

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 (126) hide show
  1. package/cli/test/smokehouse/frontends/smokehouse-bin.js +38 -22
  2. package/cli/test/smokehouse/lighthouse-runners/bundle.d.ts +1 -2
  3. package/cli/test/smokehouse/lighthouse-runners/bundle.js +11 -24
  4. package/cli/test/smokehouse/lighthouse-runners/cli.d.ts +1 -2
  5. package/cli/test/smokehouse/lighthouse-runners/cli.js +3 -7
  6. package/cli/test/smokehouse/lighthouse-runners/devtools.d.ts +1 -2
  7. package/cli/test/smokehouse/lighthouse-runners/devtools.js +1 -11
  8. package/cli/test/smokehouse/readme.md +0 -2
  9. package/cli/test/smokehouse/report-assert.d.ts +1 -2
  10. package/cli/test/smokehouse/report-assert.js +2 -19
  11. package/cli/test/smokehouse/smokehouse.js +3 -32
  12. package/core/audits/accessibility/aria-dialog-name.d.ts +10 -0
  13. package/core/audits/accessibility/aria-dialog-name.js +45 -0
  14. package/core/audits/accessibility/aria-text.d.ts +10 -0
  15. package/core/audits/accessibility/aria-text.js +44 -0
  16. package/core/audits/accessibility/empty-heading.d.ts +10 -0
  17. package/core/audits/accessibility/empty-heading.js +45 -0
  18. package/core/audits/accessibility/identical-links-same-purpose.d.ts +10 -0
  19. package/core/audits/accessibility/identical-links-same-purpose.js +45 -0
  20. package/core/audits/accessibility/landmark-one-main.d.ts +10 -0
  21. package/core/audits/accessibility/landmark-one-main.js +44 -0
  22. package/core/audits/accessibility/link-in-text-block.d.ts +10 -0
  23. package/core/audits/accessibility/link-in-text-block.js +44 -0
  24. package/core/audits/accessibility/select-name.d.ts +10 -0
  25. package/core/audits/accessibility/select-name.js +44 -0
  26. package/core/audits/accessibility/target-size.d.ts +10 -0
  27. package/core/audits/accessibility/target-size.js +45 -0
  28. package/core/audits/audit.d.ts +6 -0
  29. package/core/audits/audit.js +12 -0
  30. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +22 -6
  31. package/core/audits/byte-efficiency/byte-efficiency-audit.js +97 -25
  32. package/core/audits/byte-efficiency/render-blocking-resources.js +1 -0
  33. package/core/audits/long-tasks.d.ts +48 -0
  34. package/core/audits/long-tasks.js +146 -19
  35. package/core/audits/non-composited-animations.js +7 -0
  36. package/core/audits/prioritize-lcp-image.js +2 -1
  37. package/core/audits/redirects.js +4 -0
  38. package/core/audits/seo/link-text.js +10 -0
  39. package/core/audits/unsized-images.js +3 -0
  40. package/core/audits/viewport.js +11 -0
  41. package/core/audits/work-during-interaction.js +13 -2
  42. package/core/computed/js-bundles.js +1 -1
  43. package/core/computed/metrics/tbt-utils.d.ts +26 -0
  44. package/core/computed/metrics/tbt-utils.js +48 -28
  45. package/core/computed/metrics/total-blocking-time.js +1 -1
  46. package/core/computed/tbt-impact-tasks.d.ts +54 -0
  47. package/core/computed/tbt-impact-tasks.js +221 -0
  48. package/core/config/default-config.js +17 -0
  49. package/core/config/filters.d.ts +9 -9
  50. package/core/config/filters.js +7 -7
  51. package/core/config/validation.js +12 -0
  52. package/core/gather/base-gatherer.d.ts +1 -3
  53. package/core/gather/base-gatherer.js +1 -3
  54. package/core/gather/gatherers/accessibility.js +8 -3
  55. package/core/gather/gatherers/seo/font-size.d.ts +0 -1
  56. package/core/gather/gatherers/seo/font-size.js +0 -1
  57. package/core/gather/gatherers/source-maps.js +3 -2
  58. package/core/lib/cdt/SDK.d.ts +1 -1
  59. package/core/lib/cdt/SDK.js +2 -2
  60. package/core/lib/dependency-graph/simulator/simulator.js +3 -1
  61. package/core/lib/lh-error.d.ts +14 -2
  62. package/core/lib/lh-error.js +7 -1
  63. package/core/lib/navigation-error.d.ts +2 -2
  64. package/core/lib/navigation-error.js +1 -1
  65. package/core/lib/network-recorder.js +1 -0
  66. package/core/lib/stack-packs.js +4 -0
  67. package/dist/report/bundle.esm.js +2 -1
  68. package/dist/report/flow.js +9 -9
  69. package/dist/report/standalone.js +2 -2
  70. package/package.json +5 -5
  71. package/report/assets/styles.css +5 -1
  72. package/report/renderer/category-renderer.js +1 -0
  73. package/report/renderer/components.js +1 -1
  74. package/shared/localization/locales/ar-XB.json +233 -2
  75. package/shared/localization/locales/ar.json +233 -2
  76. package/shared/localization/locales/bg.json +233 -2
  77. package/shared/localization/locales/ca.json +233 -2
  78. package/shared/localization/locales/cs.json +246 -15
  79. package/shared/localization/locales/da.json +239 -8
  80. package/shared/localization/locales/de.json +238 -7
  81. package/shared/localization/locales/el.json +234 -3
  82. package/shared/localization/locales/en-GB.json +233 -2
  83. package/shared/localization/locales/en-US.json +87 -0
  84. package/shared/localization/locales/en-XA.json +233 -2
  85. package/shared/localization/locales/en-XL.json +87 -0
  86. package/shared/localization/locales/es-419.json +233 -2
  87. package/shared/localization/locales/es.json +233 -2
  88. package/shared/localization/locales/fi.json +233 -2
  89. package/shared/localization/locales/fil.json +236 -5
  90. package/shared/localization/locales/fr.json +235 -4
  91. package/shared/localization/locales/he.json +233 -2
  92. package/shared/localization/locales/hi.json +236 -5
  93. package/shared/localization/locales/hr.json +234 -3
  94. package/shared/localization/locales/hu.json +233 -2
  95. package/shared/localization/locales/id.json +233 -2
  96. package/shared/localization/locales/it.json +234 -3
  97. package/shared/localization/locales/ja.json +233 -2
  98. package/shared/localization/locales/ko.json +233 -2
  99. package/shared/localization/locales/lt.json +233 -2
  100. package/shared/localization/locales/lv.json +250 -19
  101. package/shared/localization/locales/nl.json +233 -2
  102. package/shared/localization/locales/no.json +235 -4
  103. package/shared/localization/locales/pl.json +233 -2
  104. package/shared/localization/locales/pt-PT.json +233 -2
  105. package/shared/localization/locales/pt.json +234 -3
  106. package/shared/localization/locales/ro.json +233 -2
  107. package/shared/localization/locales/ru.json +233 -2
  108. package/shared/localization/locales/sk.json +233 -2
  109. package/shared/localization/locales/sl.json +233 -2
  110. package/shared/localization/locales/sr-Latn.json +233 -2
  111. package/shared/localization/locales/sr.json +233 -2
  112. package/shared/localization/locales/sv.json +233 -2
  113. package/shared/localization/locales/ta.json +233 -2
  114. package/shared/localization/locales/te.json +233 -2
  115. package/shared/localization/locales/th.json +233 -2
  116. package/shared/localization/locales/tr.json +233 -2
  117. package/shared/localization/locales/uk.json +233 -2
  118. package/shared/localization/locales/vi.json +233 -2
  119. package/shared/localization/locales/zh-HK.json +233 -2
  120. package/shared/localization/locales/zh-TW.json +235 -4
  121. package/shared/localization/locales/zh.json +239 -8
  122. package/tsconfig.json +1 -1
  123. package/types/artifacts.d.ts +9 -2
  124. package/types/internal/smokehouse.d.ts +1 -3
  125. package/core/audits/metrics/first-contentful-paint-3g.d.ts +0 -15
  126. package/core/audits/metrics/first-contentful-paint-3g.js +0 -69
@@ -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
@@ -6,6 +6,52 @@
6
6
 
7
7
  const BLOCKING_TIME_THRESHOLD = 50;
8
8
 
9
+ /**
10
+ * For TBT, We only want to consider tasks that fall in our time range
11
+ * - FCP and TTI for navigation mode
12
+ * - Trace start and trace end for timespan mode
13
+ *
14
+ * FCP is picked as `startTimeMs` because there is little risk of user input happening
15
+ * before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
16
+ * optimizing to reach FCP as fast as possible without having to worry about task lengths.
17
+ *
18
+ * TTI is picked as `endTimeMs` because we want a well defined end point for page load.
19
+ *
20
+ * @param {{start: number, end: number, duration: number}} event
21
+ * @param {number} startTimeMs Should be FCP in navigation mode and the trace start time in timespan mode
22
+ * @param {number} endTimeMs Should be TTI in navigation mode and the trace end time in timespan mode
23
+ * @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`.
24
+ * @return {number}
25
+ */
26
+ function calculateTbtImpactForEvent(event, startTimeMs, endTimeMs, topLevelEvent) {
27
+ let threshold = BLOCKING_TIME_THRESHOLD;
28
+
29
+ // If a task is not top level, it doesn't make sense to subtract the entire 50ms
30
+ // blocking threshold from the event.
31
+ //
32
+ // e.g. A 80ms top level task with two 40ms children should attribute some blocking
33
+ // time to the 40ms tasks even though they do not meet the 50ms threshold.
34
+ //
35
+ // The solution is to scale the threshold for child events to be considered blocking.
36
+ if (topLevelEvent) threshold *= (event.duration / topLevelEvent.duration);
37
+
38
+ if (event.duration < threshold) return 0;
39
+ if (event.end < startTimeMs) return 0;
40
+ if (event.start > endTimeMs) return 0;
41
+
42
+ // Perform the clipping and then calculate Blocking Region. So if we have a 150ms task
43
+ // [0, 150] and `startTimeMs` is at 50ms, we first clip the task to [50, 150], and then
44
+ // calculate the Blocking Region to be [100, 150]. The rational here is that tasks before
45
+ // the start time are unimportant, so we care whether the main thread is busy more than
46
+ // 50ms at a time only after the start time.
47
+ const clippedStart = Math.max(event.start, startTimeMs);
48
+ const clippedEnd = Math.min(event.end, endTimeMs);
49
+ const clippedDuration = clippedEnd - clippedStart;
50
+ if (clippedDuration < threshold) return 0;
51
+
52
+ return clippedDuration - threshold;
53
+ }
54
+
9
55
  /**
10
56
  * @param {Array<{start: number, end: number, duration: number}>} topLevelEvents
11
57
  * @param {number} startTimeMs
@@ -15,36 +61,9 @@ const BLOCKING_TIME_THRESHOLD = 50;
15
61
  function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
16
62
  if (endTimeMs <= startTimeMs) return 0;
17
63
 
18
- const threshold = BLOCKING_TIME_THRESHOLD;
19
64
  let sumBlockingTime = 0;
20
65
  for (const event of topLevelEvents) {
21
- // Early exit for small tasks, which should far outnumber long tasks.
22
- if (event.duration < threshold) continue;
23
-
24
- // We only want to consider tasks that fall in our time range (FCP and TTI for navigations).
25
- // FCP is picked as the lower bound because there is little risk of user input happening
26
- // before FCP so Long Queuing Qelay regions do not harm user experience. Developers should be
27
- // optimizing to reach FCP as fast as possible without having to worry about task lengths.
28
- if (event.end < startTimeMs) continue;
29
-
30
- // TTI is picked as the upper bound because we want a well defined end point for page load.
31
- if (event.start > endTimeMs) continue;
32
-
33
- // We first perform the clipping, and then calculate Blocking Region. So if we have a 150ms
34
- // task [0, 150] and FCP happens midway at 50ms, we first clip the task to [50, 150], and then
35
- // calculate the Blocking Region to be [100, 150]. The rational here is that tasks before FCP
36
- // are unimportant, so we care whether the main thread is busy more than 50ms at a time only
37
- // after FCP.
38
- const clippedStart = Math.max(event.start, startTimeMs);
39
- const clippedEnd = Math.min(event.end, endTimeMs);
40
- const clippedDuration = clippedEnd - clippedStart;
41
- if (clippedDuration < threshold) continue;
42
-
43
- // The duration of the task beyond 50ms at the beginning is considered the Blocking Region.
44
- // Example:
45
- // [ 250ms Task ]
46
- // | First 50ms | Blocking Region (200ms) |
47
- sumBlockingTime += clippedDuration - threshold;
66
+ sumBlockingTime += calculateTbtImpactForEvent(event, startTimeMs, endTimeMs);
48
67
  }
49
68
 
50
69
  return sumBlockingTime;
@@ -53,4 +72,5 @@ function calculateSumOfBlockingTime(topLevelEvents, startTimeMs, endTimeMs) {
53
72
  export {
54
73
  BLOCKING_TIME_THRESHOLD,
55
74
  calculateSumOfBlockingTime,
75
+ calculateTbtImpactForEvent,
56
76
  };
@@ -60,7 +60,7 @@ class TotalBlockingTime extends ComputedMetric {
60
60
  timing: calculateSumOfBlockingTime(
61
61
  events,
62
62
  0,
63
- data.processedTrace.timestamps.traceEnd
63
+ data.processedTrace.timings.traceEnd
64
64
  ),
65
65
  };
66
66
  }
@@ -0,0 +1,54 @@
1
+ export { TBTImpactTasksComputed as TBTImpactTasks };
2
+ export type TBTImpactTask = LH.Artifacts.TaskNode & {
3
+ tbtImpact: number;
4
+ selfTbtImpact: number;
5
+ };
6
+ declare const TBTImpactTasksComputed: typeof TBTImpactTasks & {
7
+ request: (dependencies: import("../index.js").Artifacts.MetricComputationDataInput, context: import("../../types/utility-types.js").default.ImmutableObject<{
8
+ computedCache: Map<string, import("../lib/arbitrary-equality-map.js").ArbitraryEqualityMap>;
9
+ }>) => Promise<TBTImpactTask[]>;
10
+ };
11
+ /** @typedef {LH.Artifacts.TaskNode & {tbtImpact: number, selfTbtImpact: number}} TBTImpactTask */
12
+ declare class TBTImpactTasks {
13
+ /**
14
+ * @param {LH.Artifacts.TaskNode} task
15
+ * @return {LH.Artifacts.TaskNode}
16
+ */
17
+ static getTopLevelTask(task: LH.Artifacts.TaskNode): LH.Artifacts.TaskNode;
18
+ /**
19
+ * @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
20
+ * @param {LH.Artifacts.ComputedContext} context
21
+ * @return {Promise<{startTimeMs: number, endTimeMs: number}>}
22
+ */
23
+ static getTbtBounds(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
24
+ startTimeMs: number;
25
+ endTimeMs: number;
26
+ }>;
27
+ /**
28
+ * @param {LH.Artifacts.TaskNode[]} tasks
29
+ * @param {Map<LH.Artifacts.TaskNode, number>} taskToImpact
30
+ */
31
+ static createImpactTasks(tasks: LH.Artifacts.TaskNode[], taskToImpact: Map<LH.Artifacts.TaskNode, number>): TBTImpactTask[];
32
+ /**
33
+ * @param {LH.Artifacts.TaskNode[]} tasks
34
+ * @param {number} startTimeMs
35
+ * @param {number} endTimeMs
36
+ * @return {TBTImpactTask[]}
37
+ */
38
+ static computeImpactsFromObservedTasks(tasks: LH.Artifacts.TaskNode[], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
39
+ /**
40
+ * @param {LH.Artifacts.TaskNode[]} tasks
41
+ * @param {LH.Gatherer.Simulation.Result['nodeTimings']} tbtNodeTimings
42
+ * @param {number} startTimeMs
43
+ * @param {number} endTimeMs
44
+ * @return {TBTImpactTask[]}
45
+ */
46
+ static computeImpactsFromLantern(tasks: LH.Artifacts.TaskNode[], tbtNodeTimings: LH.Gatherer.Simulation.Result['nodeTimings'], startTimeMs: number, endTimeMs: number): TBTImpactTask[];
47
+ /**
48
+ * @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
49
+ * @param {LH.Artifacts.ComputedContext} context
50
+ * @return {Promise<TBTImpactTask[]>}
51
+ */
52
+ static compute_(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<TBTImpactTask[]>;
53
+ }
54
+ //# sourceMappingURL=tbt-impact-tasks.d.ts.map