lighthouse 11.7.0-dev.20240408 → 11.7.1

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 (141) hide show
  1. package/CONTRIBUTING.md +2 -2
  2. package/cli/test/smokehouse/core-tests.js +2 -0
  3. package/core/audits/accessibility/duplicate-id-active.d.ts +10 -0
  4. package/core/audits/accessibility/duplicate-id-active.js +42 -0
  5. package/core/audits/accessibility/target-size.js +1 -0
  6. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +16 -0
  7. package/core/audits/byte-efficiency/byte-efficiency-audit.js +38 -1
  8. package/core/audits/byte-efficiency/offscreen-images.d.ts +11 -0
  9. package/core/audits/byte-efficiency/offscreen-images.js +15 -0
  10. package/core/audits/dobetterweb/uses-http2.d.ts +13 -0
  11. package/core/audits/dobetterweb/uses-http2.js +34 -2
  12. package/core/audits/layout-shift-elements.d.ts +16 -0
  13. package/core/audits/layout-shift-elements.js +101 -0
  14. package/core/audits/no-unload-listeners.d.ts +16 -0
  15. package/core/audits/no-unload-listeners.js +86 -0
  16. package/core/audits/preload-fonts.d.ts +5 -1
  17. package/core/audits/preload-fonts.js +10 -1
  18. package/core/audits/script-elements-test-audit.d.ts +14 -0
  19. package/core/audits/script-elements-test-audit.js +29 -0
  20. package/core/audits/seo/is-crawlable.d.ts +1 -2
  21. package/core/audits/seo/plugins.d.ts +15 -0
  22. package/core/audits/seo/plugins.js +150 -0
  23. package/core/audits/seo/tap-targets.d.ts +50 -0
  24. package/core/audits/seo/tap-targets.js +352 -0
  25. package/core/audits/uses-rel-preload.d.ts +5 -1
  26. package/core/audits/uses-rel-preload.js +11 -1
  27. package/core/audits/work-during-interaction.d.ts +2 -2
  28. package/core/audits/work-during-interaction.js +2 -2
  29. package/core/computed/js-bundles.d.ts +1 -1
  30. package/core/computed/metrics/lantern-first-contentful-paint.d.ts +82 -13
  31. package/core/computed/metrics/lantern-first-contentful-paint.js +178 -15
  32. package/core/computed/metrics/lantern-first-meaningful-paint.d.ts +12 -13
  33. package/core/computed/metrics/lantern-first-meaningful-paint.js +49 -10
  34. package/core/computed/metrics/lantern-interactive.d.ts +21 -13
  35. package/core/computed/metrics/lantern-interactive.js +84 -11
  36. package/core/computed/metrics/lantern-largest-contentful-paint.d.ts +25 -13
  37. package/core/computed/metrics/lantern-largest-contentful-paint.js +80 -12
  38. package/core/computed/metrics/lantern-max-potential-fid.d.ts +24 -13
  39. package/core/computed/metrics/lantern-max-potential-fid.js +64 -11
  40. package/core/computed/metrics/lantern-metric.d.ts +18 -9
  41. package/core/computed/metrics/lantern-metric.js +31 -13
  42. package/core/computed/metrics/lantern-speed-index.d.ts +32 -13
  43. package/core/computed/metrics/lantern-speed-index.js +116 -12
  44. package/core/computed/metrics/lantern-total-blocking-time.d.ts +25 -13
  45. package/core/computed/metrics/lantern-total-blocking-time.js +94 -10
  46. package/core/computed/metrics/metric.js +3 -1
  47. package/core/computed/module-duplication.d.ts +1 -1
  48. package/core/computed/page-dependency-graph.js +1 -2
  49. package/core/config/default-config.js +74 -46
  50. package/core/config/experimental-config.js +0 -11
  51. package/core/config/metrics-to-audits.d.ts +19 -0
  52. package/core/config/metrics-to-audits.js +62 -0
  53. package/core/gather/gatherers/accessibility.js +3 -1
  54. package/core/gather/gatherers/global-listeners.d.ts +27 -0
  55. package/core/gather/gatherers/global-listeners.js +108 -0
  56. package/core/gather/gatherers/script-elements.d.ts +21 -0
  57. package/core/gather/gatherers/script-elements.js +100 -0
  58. package/core/gather/gatherers/seo/embedded-content.d.ts +10 -0
  59. package/core/gather/gatherers/seo/embedded-content.js +63 -0
  60. package/core/gather/gatherers/seo/tap-targets.d.ts +21 -0
  61. package/core/gather/gatherers/seo/tap-targets.js +389 -0
  62. package/core/gather/gatherers/trace-elements.d.ts +10 -0
  63. package/core/gather/gatherers/trace-elements.js +19 -0
  64. package/core/lib/lantern/page-dependency-graph.d.ts +4 -4
  65. package/core/lib/lantern/page-dependency-graph.js +5 -5
  66. package/core/lib/lighthouse-compatibility.js +10 -53
  67. package/dist/report/bundle.esm.js +1 -1
  68. package/dist/report/flow.js +14 -14
  69. package/dist/report/standalone.js +1 -1
  70. package/flow-report/src/summary/category.tsx +12 -10
  71. package/package.json +1 -1
  72. package/report/renderer/performance-category-renderer.d.ts +7 -0
  73. package/report/renderer/performance-category-renderer.js +11 -1
  74. package/report/renderer/report-utils.js +10 -13
  75. package/shared/localization/locales/ar-XB.json +64 -1
  76. package/shared/localization/locales/ar.json +64 -1
  77. package/shared/localization/locales/bg.json +64 -1
  78. package/shared/localization/locales/ca.json +64 -1
  79. package/shared/localization/locales/cs.json +64 -1
  80. package/shared/localization/locales/da.json +64 -1
  81. package/shared/localization/locales/de.json +64 -1
  82. package/shared/localization/locales/el.json +64 -1
  83. package/shared/localization/locales/en-GB.json +64 -1
  84. package/shared/localization/locales/en-US.json +65 -2
  85. package/shared/localization/locales/en-XA.json +64 -1
  86. package/shared/localization/locales/en-XL.json +65 -2
  87. package/shared/localization/locales/es-419.json +64 -1
  88. package/shared/localization/locales/es.json +64 -1
  89. package/shared/localization/locales/fi.json +64 -1
  90. package/shared/localization/locales/fil.json +64 -1
  91. package/shared/localization/locales/fr.json +64 -1
  92. package/shared/localization/locales/he.json +64 -1
  93. package/shared/localization/locales/hi.json +64 -1
  94. package/shared/localization/locales/hr.json +64 -1
  95. package/shared/localization/locales/hu.json +64 -1
  96. package/shared/localization/locales/id.json +64 -1
  97. package/shared/localization/locales/it.json +64 -1
  98. package/shared/localization/locales/ja.json +64 -1
  99. package/shared/localization/locales/ko.json +64 -1
  100. package/shared/localization/locales/lt.json +64 -1
  101. package/shared/localization/locales/lv.json +64 -1
  102. package/shared/localization/locales/nl.json +64 -1
  103. package/shared/localization/locales/no.json +64 -1
  104. package/shared/localization/locales/pl.json +64 -1
  105. package/shared/localization/locales/pt-PT.json +64 -1
  106. package/shared/localization/locales/pt.json +64 -1
  107. package/shared/localization/locales/ro.json +64 -1
  108. package/shared/localization/locales/ru.json +64 -1
  109. package/shared/localization/locales/sk.json +64 -1
  110. package/shared/localization/locales/sl.json +64 -1
  111. package/shared/localization/locales/sr-Latn.json +64 -1
  112. package/shared/localization/locales/sr.json +64 -1
  113. package/shared/localization/locales/sv.json +64 -1
  114. package/shared/localization/locales/ta.json +64 -1
  115. package/shared/localization/locales/te.json +64 -1
  116. package/shared/localization/locales/th.json +64 -1
  117. package/shared/localization/locales/tr.json +64 -1
  118. package/shared/localization/locales/uk.json +64 -1
  119. package/shared/localization/locales/vi.json +64 -1
  120. package/shared/localization/locales/zh-HK.json +64 -1
  121. package/shared/localization/locales/zh-TW.json +64 -1
  122. package/shared/localization/locales/zh.json +64 -1
  123. package/tsconfig.json +0 -1
  124. package/types/artifacts.d.ts +17 -3
  125. package/types/config.d.ts +1 -0
  126. package/types/lhr/audit-details.d.ts +4 -9
  127. package/types/lhr/lhr.d.ts +2 -0
  128. package/core/lib/lantern/metrics/first-contentful-paint.d.ts +0 -79
  129. package/core/lib/lantern/metrics/first-contentful-paint.js +0 -200
  130. package/core/lib/lantern/metrics/first-meaningful-paint.d.ts +0 -6
  131. package/core/lib/lantern/metrics/first-meaningful-paint.js +0 -66
  132. package/core/lib/lantern/metrics/interactive.d.ts +0 -26
  133. package/core/lib/lantern/metrics/interactive.js +0 -112
  134. package/core/lib/lantern/metrics/largest-contentful-paint.d.ts +0 -19
  135. package/core/lib/lantern/metrics/largest-contentful-paint.js +0 -108
  136. package/core/lib/lantern/metrics/max-potential-fid.d.ts +0 -30
  137. package/core/lib/lantern/metrics/max-potential-fid.js +0 -92
  138. package/core/lib/lantern/metrics/speed-index.d.ts +0 -38
  139. package/core/lib/lantern/metrics/speed-index.js +0 -145
  140. package/core/lib/lantern/metrics/total-blocking-time.d.ts +0 -31
  141. package/core/lib/lantern/metrics/total-blocking-time.js +0 -128
@@ -0,0 +1,389 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2018 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /* global document, window, getComputedStyle, getElementsInDocument, Node, getNodeDetails, getRectCenterPoint */
8
+
9
+ import BaseGatherer from '../../base-gatherer.js';
10
+ import {pageFunctions} from '../../../lib/page-functions.js';
11
+ import * as RectHelpers from '../../../lib/rect-helpers.js';
12
+
13
+ const TARGET_SELECTORS = [
14
+ 'button',
15
+ 'a',
16
+ 'input',
17
+ 'textarea',
18
+ 'select',
19
+ 'option',
20
+ '[role=button]',
21
+ '[role=checkbox]',
22
+ '[role=link]',
23
+ '[role=menuitem]',
24
+ '[role=menuitemcheckbox]',
25
+ '[role=menuitemradio]',
26
+ '[role=option]',
27
+ '[role=scrollbar]',
28
+ '[role=slider]',
29
+ '[role=spinbutton]',
30
+ ];
31
+ const tapTargetsSelector = TARGET_SELECTORS.join(',');
32
+
33
+ /**
34
+ * @param {HTMLElement} element
35
+ * @return {boolean}
36
+ */
37
+ /* c8 ignore start */
38
+ function elementIsVisible(element) {
39
+ return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
40
+ }
41
+ /* c8 ignore stop */
42
+
43
+ /**
44
+ * @param {Element} element
45
+ * @return {LH.Artifacts.Rect[]}
46
+ */
47
+ /* c8 ignore start */
48
+ function getClientRects(element) {
49
+ const clientRects = Array.from(
50
+ element.getClientRects()
51
+ ).map(clientRect => {
52
+ // Contents of DOMRect get lost when returned from Runtime.evaluate call,
53
+ // so we convert them to plain objects.
54
+ const {width, height, left, top, right, bottom} = clientRect;
55
+ return {width, height, left, top, right, bottom};
56
+ });
57
+
58
+ for (const child of element.children) {
59
+ clientRects.push(...getClientRects(child));
60
+ }
61
+
62
+ return clientRects;
63
+ }
64
+ /* c8 ignore stop */
65
+
66
+ /**
67
+ * @param {Element} element
68
+ * @param {string} tapTargetsSelector
69
+ * @return {boolean}
70
+ */
71
+ /* c8 ignore start */
72
+ function elementHasAncestorTapTarget(element, tapTargetsSelector) {
73
+ if (!element.parentElement) {
74
+ return false;
75
+ }
76
+ if (element.parentElement.matches(tapTargetsSelector)) {
77
+ return true;
78
+ }
79
+ return elementHasAncestorTapTarget(element.parentElement, tapTargetsSelector);
80
+ }
81
+ /* c8 ignore stop */
82
+
83
+ /**
84
+ * @param {Element} element
85
+ */
86
+ /* c8 ignore start */
87
+ function hasTextNodeSiblingsFormingTextBlock(element) {
88
+ if (!element.parentElement) {
89
+ return false;
90
+ }
91
+
92
+ const parentElement = element.parentElement;
93
+
94
+ const nodeText = element.textContent || '';
95
+ const parentText = parentElement.textContent || '';
96
+ if (parentText.length - nodeText.length < 5) {
97
+ // Parent text mostly consists of this node, so the parent
98
+ // is not a text block container
99
+ return false;
100
+ }
101
+
102
+ for (const sibling of element.parentElement.childNodes) {
103
+ if (sibling === element) {
104
+ continue;
105
+ }
106
+ const siblingTextContent = (sibling.textContent || '').trim();
107
+ // Only count text in text nodes so that a series of e.g. buttons isn't counted
108
+ // as a text block.
109
+ // This works reasonably well, but means we miss text blocks where all text is e.g.
110
+ // wrapped in spans
111
+ if (sibling.nodeType === Node.TEXT_NODE && siblingTextContent.length > 0) {
112
+ return true;
113
+ }
114
+ }
115
+
116
+ return false;
117
+ }
118
+ /* c8 ignore stop */
119
+
120
+ /**
121
+ * Check if element is in a block of text, such as paragraph with a bunch of links in it.
122
+ * Makes a reasonable guess, but for example gets it wrong if the element is surrounded by other
123
+ * HTML elements instead of direct text nodes.
124
+ * @param {Element} element
125
+ * @return {boolean}
126
+ */
127
+ /* c8 ignore start */
128
+ function elementIsInTextBlock(element) {
129
+ const {display} = getComputedStyle(element);
130
+ if (display !== 'inline' && display !== 'inline-block') {
131
+ return false;
132
+ }
133
+
134
+ if (hasTextNodeSiblingsFormingTextBlock(element)) {
135
+ return true;
136
+ } else if (element.parentElement) {
137
+ return elementIsInTextBlock(element.parentElement);
138
+ } else {
139
+ return false;
140
+ }
141
+ }
142
+ /* c8 ignore stop */
143
+
144
+ /**
145
+ * @param {Element} el
146
+ * @param {{x: number, y: number}} elCenterPoint
147
+ */
148
+ /* c8 ignore start */
149
+ function elementCenterIsAtZAxisTop(el, elCenterPoint) {
150
+ const viewportHeight = window.innerHeight;
151
+ const targetScrollY = Math.floor(elCenterPoint.y / viewportHeight) * viewportHeight;
152
+ if (window.scrollY !== targetScrollY) {
153
+ window.scrollTo(0, targetScrollY);
154
+ }
155
+
156
+ const topEl = document.elementFromPoint(
157
+ elCenterPoint.x,
158
+ elCenterPoint.y - window.scrollY
159
+ );
160
+
161
+ return topEl === el || el.contains(topEl);
162
+ }
163
+ /* c8 ignore stop */
164
+
165
+ /**
166
+ * Finds all position sticky/absolute elements on the page and adds a class
167
+ * that disables pointer events on them.
168
+ * @param {string} className
169
+ * @return {() => void} - undo function to re-enable pointer events
170
+ */
171
+ /* c8 ignore start */
172
+ function disableFixedAndStickyElementPointerEvents(className) {
173
+ document.querySelectorAll('*').forEach(el => {
174
+ const position = getComputedStyle(el).position;
175
+ if (position === 'fixed' || position === 'sticky') {
176
+ el.classList.add(className);
177
+ }
178
+ });
179
+
180
+ return function undo() {
181
+ Array.from(document.getElementsByClassName(className)).forEach(el => {
182
+ el.classList.remove(className);
183
+ });
184
+ };
185
+ }
186
+ /* c8 ignore stop */
187
+
188
+ /**
189
+ * @param {string} tapTargetsSelector
190
+ * @param {string} className
191
+ * @return {LH.Artifacts.TapTarget[]}
192
+ */
193
+ /* c8 ignore start */
194
+ function gatherTapTargets(tapTargetsSelector, className) {
195
+ /** @type {LH.Artifacts.TapTarget[]} */
196
+ const targets = [];
197
+
198
+ // Capture element positions relative to the top of the page
199
+ window.scrollTo(0, 0);
200
+
201
+ /** @type {HTMLElement[]} */
202
+ // @ts-expect-error - getElementsInDocument put into scope via stringification
203
+ const tapTargetElements = getElementsInDocument(tapTargetsSelector);
204
+
205
+ /** @type {{
206
+ tapTargetElement: Element,
207
+ clientRects: LH.Artifacts.Rect[]
208
+ }[]} */
209
+ const tapTargetsWithClientRects = [];
210
+ tapTargetElements.forEach(tapTargetElement => {
211
+ // Filter out tap targets that are likely to cause false failures:
212
+ if (elementHasAncestorTapTarget(tapTargetElement, tapTargetsSelector)) {
213
+ // This is usually intentional, either the tap targets trigger the same action
214
+ // or there's a child with a related action (like a delete button for an item)
215
+ return;
216
+ }
217
+ if (elementIsInTextBlock(tapTargetElement)) {
218
+ // Links inside text blocks cause a lot of failures, and there's also an exception for them
219
+ // in the Web Content Accessibility Guidelines https://www.w3.org/TR/WCAG21/#target-size
220
+ return;
221
+ }
222
+ if (!elementIsVisible(tapTargetElement)) {
223
+ return;
224
+ }
225
+
226
+ tapTargetsWithClientRects.push({
227
+ tapTargetElement,
228
+ clientRects: getClientRects(tapTargetElement),
229
+ });
230
+ });
231
+
232
+ // Disable pointer events so that tap targets below them don't get
233
+ // detected as non-tappable (they are tappable, just not while the viewport
234
+ // is at the current scroll position)
235
+ const reenableFixedAndStickyElementPointerEvents =
236
+ disableFixedAndStickyElementPointerEvents(className);
237
+
238
+ /** @type {{
239
+ tapTargetElement: Element,
240
+ visibleClientRects: LH.Artifacts.Rect[]
241
+ }[]} */
242
+ const tapTargetsWithVisibleClientRects = [];
243
+ // We use separate loop here to get visible client rects because that involves
244
+ // scrolling around the page for elementCenterIsAtZAxisTop, which would affect the
245
+ // client rect positions.
246
+ tapTargetsWithClientRects.forEach(({tapTargetElement, clientRects}) => {
247
+ // Filter out empty client rects
248
+ let visibleClientRects = clientRects.filter(cr => cr.width !== 0 && cr.height !== 0);
249
+
250
+ // Filter out client rects that are invisible, e.g because they are in a position absolute element
251
+ // with a lower z-index than the main content.
252
+ // This will also filter out all position fixed or sticky tap targets elements because we disable pointer
253
+ // events on them before running this. That's the correct behavior because whether a position fixed/stick
254
+ // element overlaps with another tap target depends on the scroll position.
255
+ visibleClientRects = visibleClientRects.filter(rect => {
256
+ // Just checking the center can cause false failures for large partially hidden tap targets,
257
+ // but that should be a rare edge case
258
+ // @ts-expect-error - put into scope via stringification
259
+ const rectCenterPoint = getRectCenterPoint(rect);
260
+ return elementCenterIsAtZAxisTop(tapTargetElement, rectCenterPoint);
261
+ });
262
+
263
+ if (visibleClientRects.length > 0) {
264
+ tapTargetsWithVisibleClientRects.push({
265
+ tapTargetElement,
266
+ visibleClientRects,
267
+ });
268
+ }
269
+ });
270
+
271
+ for (const {tapTargetElement, visibleClientRects} of tapTargetsWithVisibleClientRects) {
272
+ targets.push({
273
+ clientRects: visibleClientRects,
274
+ href: /** @type {HTMLAnchorElement} */(tapTargetElement)['href'] || '',
275
+ // @ts-expect-error - getNodeDetails put into scope via stringification
276
+ node: getNodeDetails(tapTargetElement),
277
+ });
278
+ }
279
+
280
+ reenableFixedAndStickyElementPointerEvents();
281
+
282
+ return targets;
283
+ }
284
+
285
+ /**
286
+ * @param {string} tapTargetsSelector
287
+ * @param {string} className
288
+ * @return {LH.Artifacts.TapTarget[]}
289
+ */
290
+ function gatherTapTargetsAndResetScroll(tapTargetsSelector, className) {
291
+ const originalScrollPosition = {
292
+ x: window.scrollX,
293
+ y: window.scrollY,
294
+ };
295
+
296
+ try {
297
+ return gatherTapTargets(tapTargetsSelector, className);
298
+ } finally {
299
+ window.scrollTo(originalScrollPosition.x, originalScrollPosition.y);
300
+ }
301
+ }
302
+ /* c8 ignore stop */
303
+
304
+ class TapTargets extends BaseGatherer {
305
+ constructor() {
306
+ super();
307
+ /**
308
+ * This needs to be in the constructor.
309
+ * https://github.com/GoogleChrome/lighthouse/issues/12134
310
+ * @type {LH.Gatherer.GathererMeta}
311
+ */
312
+ this.meta = {
313
+ supportedModes: ['snapshot', 'navigation'],
314
+ };
315
+ }
316
+
317
+ /**
318
+ * @param {LH.Gatherer.ProtocolSession} session
319
+ * @param {string} className
320
+ * @return {Promise<string>}
321
+ */
322
+ async addStyleRule(session, className) {
323
+ const frameTreeResponse = await session.sendCommand('Page.getFrameTree');
324
+ const {styleSheetId} = await session.sendCommand('CSS.createStyleSheet', {
325
+ frameId: frameTreeResponse.frameTree.frame.id,
326
+ });
327
+ const ruleText = `.${className} { pointer-events: none !important }`;
328
+ await session.sendCommand('CSS.setStyleSheetText', {
329
+ styleSheetId,
330
+ text: ruleText,
331
+ });
332
+ return styleSheetId;
333
+ }
334
+
335
+ /**
336
+ * @param {LH.Gatherer.ProtocolSession} session
337
+ * @param {string} styleSheetId
338
+ */
339
+ async removeStyleRule(session, styleSheetId) {
340
+ await session.sendCommand('CSS.setStyleSheetText', {
341
+ styleSheetId,
342
+ text: '',
343
+ });
344
+ }
345
+
346
+ /**
347
+ * @param {LH.Gatherer.Context} passContext
348
+ * @return {Promise<LH.Artifacts.TapTarget[]>} All visible tap targets with their positions and sizes
349
+ */
350
+ async getArtifact(passContext) {
351
+ const session = passContext.driver.defaultSession;
352
+ await session.sendCommand('DOM.enable');
353
+ await session.sendCommand('CSS.enable');
354
+
355
+ const className = 'lighthouse-disable-pointer-events';
356
+ const styleSheetId = await this.addStyleRule(session, className);
357
+
358
+ const tapTargets =
359
+ await passContext.driver.executionContext.evaluate(gatherTapTargetsAndResetScroll, {
360
+ args: [tapTargetsSelector, className],
361
+ useIsolation: true,
362
+ deps: [
363
+ pageFunctions.getNodeDetails,
364
+ pageFunctions.getElementsInDocument,
365
+ disableFixedAndStickyElementPointerEvents,
366
+ elementIsVisible,
367
+ elementHasAncestorTapTarget,
368
+ elementCenterIsAtZAxisTop,
369
+ getClientRects,
370
+ hasTextNodeSiblingsFormingTextBlock,
371
+ elementIsInTextBlock,
372
+ RectHelpers.getRectCenterPoint,
373
+ pageFunctions.getNodePath,
374
+ pageFunctions.getNodeSelector,
375
+ pageFunctions.getNodeLabel,
376
+ gatherTapTargets,
377
+ ],
378
+ });
379
+
380
+ await this.removeStyleRule(session, styleSheetId);
381
+
382
+ await session.sendCommand('CSS.disable');
383
+ await session.sendCommand('DOM.disable');
384
+
385
+ return tapTargets;
386
+ }
387
+ }
388
+
389
+ export default TapTargets;
@@ -9,6 +9,16 @@ export type TraceElementData = {
9
9
  type?: string;
10
10
  };
11
11
  declare class TraceElements extends BaseGatherer {
12
+ /**
13
+ * This function finds the top (up to 15) elements that shift on the page.
14
+ *
15
+ * @param {LH.Trace} trace
16
+ * @param {LH.Gatherer.Context} context
17
+ * @return {Promise<Array<{nodeId: number}>>}
18
+ */
19
+ static getTopLayoutShiftElements(trace: LH.Trace, context: LH.Gatherer.Context): Promise<Array<{
20
+ nodeId: number;
21
+ }>>;
12
22
  /**
13
23
  * We want to a single representative node to represent the shift, so let's pick
14
24
  * the one with the largest impact (size x distance moved).
@@ -28,6 +28,7 @@ import {TraceEngineResult} from '../../computed/trace-engine-result.js';
28
28
 
29
29
  /** @typedef {{nodeId: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[], type?: string}} TraceElementData */
30
30
 
31
+ const MAX_LAYOUT_SHIFT_ELEMENTS = 15;
31
32
  const MAX_LAYOUT_SHIFTS = 15;
32
33
 
33
34
  /**
@@ -65,6 +66,22 @@ class TraceElements extends BaseGatherer {
65
66
  if (name) this.animationIdToName.set(id, name);
66
67
  }
67
68
 
69
+ /**
70
+ * This function finds the top (up to 15) elements that shift on the page.
71
+ *
72
+ * @param {LH.Trace} trace
73
+ * @param {LH.Gatherer.Context} context
74
+ * @return {Promise<Array<{nodeId: number}>>}
75
+ */
76
+ static async getTopLayoutShiftElements(trace, context) {
77
+ const {impactByNodeId} = await CumulativeLayoutShift.request(trace, context);
78
+
79
+ return [...impactByNodeId.entries()]
80
+ .sort((a, b) => b[1] - a[1])
81
+ .slice(0, MAX_LAYOUT_SHIFT_ELEMENTS)
82
+ .map(([nodeId]) => ({nodeId}));
83
+ }
84
+
68
85
  /**
69
86
  * We want to a single representative node to represent the shift, so let's pick
70
87
  * the one with the largest impact (size x distance moved).
@@ -321,6 +338,7 @@ class TraceElements extends BaseGatherer {
321
338
  const {mainThreadEvents} = processedTrace;
322
339
 
323
340
  const lcpNodeData = await TraceElements.getLcpElement(trace, context);
341
+ const shiftElementsNodeData = await TraceElements.getTopLayoutShiftElements(trace, context);
324
342
  const shiftsData = await TraceElements.getTopLayoutShifts(
325
343
  trace, traceEngineResult, rootCauses, context);
326
344
  const animatedElementData = await this.getAnimatedElements(mainThreadEvents);
@@ -329,6 +347,7 @@ class TraceElements extends BaseGatherer {
329
347
  /** @type {Map<string, TraceElementData[]>} */
330
348
  const backendNodeDataMap = new Map([
331
349
  ['largest-contentful-paint', lcpNodeData ? [lcpNodeData] : []],
350
+ ['layout-shift-element', shiftElementsNodeData],
332
351
  ['layout-shift', shiftsData],
333
352
  ['animation', animatedElementData],
334
353
  ['responsiveness', responsivenessElementData ? [responsivenessElementData] : []],
@@ -18,10 +18,10 @@ export class PageDependencyGraph {
18
18
  */
19
19
  static getNetworkNodeOutput(networkRecords: Array<Lantern.NetworkRequest>): NetworkNodeOutput;
20
20
  /**
21
- * @param {LH.TraceEvent[]} mainThreadEvents
21
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
22
22
  * @return {Array<CPUNode>}
23
23
  */
24
- static getCPUNodes(mainThreadEvents: LH.TraceEvent[]): Array<CPUNode>;
24
+ static getCPUNodes({ mainThreadEvents }: LH.Artifacts.ProcessedTrace): Array<CPUNode>;
25
25
  /**
26
26
  * @param {NetworkNode} rootNode
27
27
  * @param {NetworkNodeOutput} networkNodeOutput
@@ -40,12 +40,12 @@ export class PageDependencyGraph {
40
40
  */
41
41
  static _pruneNode(node: Node): void;
42
42
  /**
43
- * @param {LH.TraceEvent[]} mainThreadEvents
43
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
44
44
  * @param {Array<Lantern.NetworkRequest>} networkRecords
45
45
  * @param {URLArtifact} URL
46
46
  * @return {Node}
47
47
  */
48
- static createGraph(mainThreadEvents: LH.TraceEvent[], networkRecords: Array<Lantern.NetworkRequest>, URL: URLArtifact): Node;
48
+ static createGraph(processedTrace: LH.Artifacts.ProcessedTrace, networkRecords: Array<Lantern.NetworkRequest>, URL: URLArtifact): Node;
49
49
  /**
50
50
  *
51
51
  * @param {Node} rootNode
@@ -109,10 +109,10 @@ class PageDependencyGraph {
109
109
  }
110
110
 
111
111
  /**
112
- * @param {LH.TraceEvent[]} mainThreadEvents
112
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
113
113
  * @return {Array<CPUNode>}
114
114
  */
115
- static getCPUNodes(mainThreadEvents) {
115
+ static getCPUNodes({mainThreadEvents}) {
116
116
  /** @type {Array<CPUNode>} */
117
117
  const nodes = [];
118
118
  let i = 0;
@@ -393,14 +393,14 @@ class PageDependencyGraph {
393
393
  }
394
394
 
395
395
  /**
396
- * @param {LH.TraceEvent[]} mainThreadEvents
396
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
397
397
  * @param {Array<Lantern.NetworkRequest>} networkRecords
398
398
  * @param {URLArtifact} URL
399
399
  * @return {Node}
400
400
  */
401
- static createGraph(mainThreadEvents, networkRecords, URL) {
401
+ static createGraph(processedTrace, networkRecords, URL) {
402
402
  const networkNodeOutput = PageDependencyGraph.getNetworkNodeOutput(networkRecords);
403
- const cpuNodes = PageDependencyGraph.getCPUNodes(mainThreadEvents);
403
+ const cpuNodes = PageDependencyGraph.getCPUNodes(processedTrace);
404
404
  const {requestedUrl, mainDocumentUrl} = URL;
405
405
  if (!requestedUrl) throw new Error('requestedUrl is required to get the root request');
406
406
  if (!mainDocumentUrl) throw new Error('mainDocumentUrl is required to get the main resource');
@@ -108,62 +108,19 @@ function upgradeLhrForCompatibility(lhr) {
108
108
  }
109
109
  }
110
110
 
111
- // This backcompat converts old LHRs to use the "hidden" and "diagnostics" groups.
112
- // <9.0.0 LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
113
- // <9.0.0 LHRs used "load-opportunities" for opportunity audits that are now grouped under "diagnostics".
114
- // >=9.0.0 && <12.0.0 LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
115
- // >=12.0.0 LHRs use the "hidden" group to identify hidden audits and "diagnostics" to identify diagnostics. "no group" is meaningless.
111
+ // This backcompat converts old LHRs (<9.0.0) to use the new "hidden" group.
112
+ // Old LHRs used "no group" to identify audits that should be hidden in performance instead of the "hidden" group.
113
+ // Newer LHRs use "no group" to identify opportunities and diagnostics whose groups are assigned by details type.
116
114
  const [majorVersion] = lhr.lighthouseVersion.split('.').map(Number);
117
115
  const perfCategory = lhr.categories['performance'];
118
- if (perfCategory) {
119
- if (majorVersion < 9) {
120
- if (!lhr.categoryGroups) lhr.categoryGroups = {};
121
- lhr.categoryGroups['hidden'] = {title: ''};
122
- for (const auditRef of perfCategory.auditRefs) {
123
- if (!auditRef.group) {
124
- auditRef.group = 'hidden';
125
- } else if (auditRef.group === 'load-opportunities') {
126
- auditRef.group = 'diagnostics';
127
- }
128
- }
129
- } else if (majorVersion < 12) {
130
- for (const auditRef of perfCategory.auditRefs) {
131
- if (!auditRef.group) {
132
- auditRef.group = 'diagnostics';
133
- }
134
- }
135
- }
136
- }
137
-
138
- if (majorVersion < 12 && perfCategory) {
139
- /** @type {Map<string, string[]>} */
140
- const metricRelevanceMap = new Map();
141
-
116
+ if (majorVersion < 9 && perfCategory) {
117
+ if (!lhr.categoryGroups) lhr.categoryGroups = {};
118
+ lhr.categoryGroups['hidden'] = {title: ''};
142
119
  for (const auditRef of perfCategory.auditRefs) {
143
- /** @type {string[]|undefined} */
144
- // @ts-expect-error Removed in v12
145
- const relevantAudits = auditRef.relevantAudits;
146
- if (!relevantAudits || !auditRef.acronym) continue;
147
-
148
- for (const auditId of relevantAudits) {
149
- const acronyms = metricRelevanceMap.get(auditId) || [];
150
- acronyms.push(auditRef.acronym);
151
- metricRelevanceMap.set(auditId, acronyms);
152
- }
153
- }
154
-
155
- for (const [auditId, acronyms] of metricRelevanceMap) {
156
- if (!acronyms.length) continue;
157
-
158
- const audit = lhr.audits[auditId];
159
- if (!audit) continue;
160
-
161
- // Old versions can still define metric savings, let's not mess with it.
162
- if (audit.metricSavings) continue;
163
-
164
- audit.metricSavings = {};
165
- for (const acronym of acronyms) {
166
- audit.metricSavings[acronym] = 0;
120
+ if (!auditRef.group) {
121
+ auditRef.group = 'hidden';
122
+ } else if (['load-opportunities', 'diagnostics'].includes(auditRef.group)) {
123
+ delete auditRef.group;
167
124
  }
168
125
  }
169
126
  }