lighthouse 12.3.0-dev.20250209 → 12.3.0-dev.20250211

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 (41) hide show
  1. package/core/audits/bootup-time.js +0 -2
  2. package/core/audits/byte-efficiency/duplicated-javascript.d.ts +4 -5
  3. package/core/audits/byte-efficiency/duplicated-javascript.js +9 -5
  4. package/core/audits/byte-efficiency/legacy-javascript.d.ts +2 -2
  5. package/core/audits/byte-efficiency/legacy-javascript.js +17 -5
  6. package/core/audits/byte-efficiency/polyfill-graph-data.json +48 -49
  7. package/core/audits/byte-efficiency/total-byte-weight.js +0 -2
  8. package/core/audits/dobetterweb/dom-size.js +0 -2
  9. package/core/audits/insights/image-delivery-insight.js +31 -6
  10. package/core/audits/insights/insight-audit.d.ts +7 -0
  11. package/core/audits/insights/insight-audit.js +35 -1
  12. package/core/audits/insights/interaction-to-next-paint-insight.js +23 -5
  13. package/core/audits/insights/lcp-phases-insight.d.ts +5 -0
  14. package/core/audits/insights/lcp-phases-insight.js +45 -11
  15. package/core/audits/insights/third-parties-insight.d.ts +17 -0
  16. package/core/audits/insights/third-parties-insight.js +44 -7
  17. package/core/audits/mainthread-work-breakdown.js +0 -2
  18. package/core/audits/seo/is-crawlable.d.ts +1 -0
  19. package/core/audits/server-response-time.js +0 -1
  20. package/core/computed/trace-engine-result.d.ts +4 -0
  21. package/core/computed/trace-engine-result.js +88 -0
  22. package/core/config/default-config.js +14 -14
  23. package/core/gather/gatherers/trace-elements.js +1 -1
  24. package/core/lib/trace-engine.d.ts +1 -0
  25. package/core/lib/trace-engine.js +2 -0
  26. package/dist/report/bundle.esm.js +13 -10
  27. package/dist/report/flow.js +8 -5
  28. package/dist/report/standalone.js +7 -4
  29. package/flow-report/src/i18n/i18n.d.ts +2 -0
  30. package/package.json +2 -2
  31. package/report/assets/styles.css +3 -0
  32. package/report/assets/templates.html +1 -0
  33. package/report/renderer/components.js +8 -2
  34. package/report/renderer/performance-category-renderer.d.ts +10 -0
  35. package/report/renderer/performance-category-renderer.js +34 -23
  36. package/report/renderer/report-utils.d.ts +1 -0
  37. package/report/renderer/report-utils.js +2 -0
  38. package/report/renderer/topbar-features.js +8 -0
  39. package/shared/localization/locales/en-US.json +12 -3
  40. package/shared/localization/locales/en-XL.json +12 -3
  41. package/types/lhr/audit-details.d.ts +2 -0
@@ -14,7 +14,6 @@ import {MainThreadTasks} from '../computed/main-thread-tasks.js';
14
14
  import {getExecutionTimingsByURL} from '../lib/tracehouse/task-summary.js';
15
15
  import {TBTImpactTasks} from '../computed/tbt-impact-tasks.js';
16
16
  import {Sentry} from '../lib/sentry.js';
17
- import {Util} from '../../shared/util.js';
18
17
 
19
18
  const UIStrings = {
20
19
  /** Title of a diagnostic audit that provides detail on the time spent executing javascript files during the load. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -173,7 +172,6 @@ class BootupTime extends Audit {
173
172
 
174
173
  return {
175
174
  score,
176
- scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
177
175
  notApplicable: !results.length,
178
176
  numericValue: totalBootupTime,
179
177
  numericUnit: 'millisecond',
@@ -26,11 +26,10 @@ declare class DuplicatedJavascript extends ByteEfficiencyAudit {
26
26
  resourceSize: number;
27
27
  }[]>>;
28
28
  /**
29
- * This audit highlights JavaScript modules that appear to be duplicated across all resources,
30
- * either within the same bundle or between different bundles. Each details item returned is
31
- * a module with subItems for each resource that includes it. The wastedBytes for the details
32
- * item is the number of bytes occupied by the sum of all but the largest copy of the module.
33
- * wastedBytesByUrl attributes the cost of the bytes to a specific resource, for use by lantern.
29
+ * Each details item returned is a module with subItems for each resource that
30
+ * includes it. The wastedBytes for the details item is the number of bytes
31
+ * occupied by the sum of all but the largest copy of the module. wastedBytesByUrl
32
+ * attributes the cost of the bytes to a specific resource, for use by lantern.
34
33
  * @param {LH.Artifacts} artifacts
35
34
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
36
35
  * @param {LH.Audit.Context} context
@@ -4,6 +4,11 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ /**
8
+ * @fileoverview This audit highlights JavaScript modules that appear to be duplicated across
9
+ * all resources, either within the same bundle or between different bundles.
10
+ */
11
+
7
12
  /** @typedef {import('./byte-efficiency-audit.js').ByteEfficiencyProduct} ByteEfficiencyProduct */
8
13
  /** @typedef {LH.Audit.ByteEfficiencyItem & {source: string, subItems: {type: 'subitems', items: SubItem[]}}} Item */
9
14
  /** @typedef {{url: string, sourceTransferBytes?: number}} SubItem */
@@ -104,11 +109,10 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
104
109
  }
105
110
 
106
111
  /**
107
- * This audit highlights JavaScript modules that appear to be duplicated across all resources,
108
- * either within the same bundle or between different bundles. Each details item returned is
109
- * a module with subItems for each resource that includes it. The wastedBytes for the details
110
- * item is the number of bytes occupied by the sum of all but the largest copy of the module.
111
- * wastedBytesByUrl attributes the cost of the bytes to a specific resource, for use by lantern.
112
+ * Each details item returned is a module with subItems for each resource that
113
+ * includes it. The wastedBytes for the details item is the number of bytes
114
+ * occupied by the sum of all but the largest copy of the module. wastedBytesByUrl
115
+ * attributes the cost of the bytes to a specific resource, for use by lantern.
112
116
  * @param {LH.Artifacts} artifacts
113
117
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
114
118
  * @param {LH.Audit.Context} context
@@ -50,11 +50,10 @@ declare class LegacyJavascript extends ByteEfficiencyAudit {
50
50
  *
51
51
  * @param {CodePatternMatcher} matcher
52
52
  * @param {LH.Artifacts['Scripts']} scripts
53
- * @param {LH.Artifacts.NetworkRequest[]} networkRecords
54
53
  * @param {LH.Artifacts.Bundle[]} bundles
55
54
  * @return {Map<LH.Artifacts.Script, PatternMatchResult[]>}
56
55
  */
57
- static detectAcrossScripts(matcher: CodePatternMatcher, scripts: LH.Artifacts["Scripts"], networkRecords: LH.Artifacts.NetworkRequest[], bundles: LH.Artifacts.Bundle[]): Map<LH.Artifacts.Script, PatternMatchResult[]>;
56
+ static detectAcrossScripts(matcher: CodePatternMatcher, scripts: LH.Artifacts["Scripts"], bundles: LH.Artifacts.Bundle[]): Map<LH.Artifacts.Script, PatternMatchResult[]>;
58
57
  /**
59
58
  * @param {PatternMatchResult[]} matches
60
59
  * @return {number}
@@ -71,6 +70,7 @@ declare class LegacyJavascript extends ByteEfficiencyAudit {
71
70
  export namespace UIStrings {
72
71
  let title: string;
73
72
  let description: string;
73
+ let detectedCoreJs2Warning: string;
74
74
  }
75
75
  import { ByteEfficiencyAudit } from './byte-efficiency-audit.js';
76
76
  /**
@@ -39,7 +39,10 @@ const UIStrings = {
39
39
  // eslint-disable-next-line max-len
40
40
  // TODO: developer.chrome.com article. this codelab is good starting place: https://web.dev/articles/codelab-serve-modern-code
41
41
  /** Description of a Lighthouse audit that tells the user about old JavaScript that is no longer needed. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
42
- description: 'Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren\'t necessary for modern browsers. For your bundled JavaScript, adopt a modern script deployment strategy using module/nomodule feature detection to reduce the amount of code shipped to modern browsers, while retaining support for legacy browsers. [Learn how to use modern JavaScript](https://web.dev/articles/publish-modern-javascript)',
42
+ description: 'Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren\'t necessary for modern browsers. For your bundled JavaScript, adopt a modern script deployment strategy using [module/nomodule feature detection](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/) to reduce the amount of code shipped to modern browsers, while retaining support for legacy browsers. [Learn how to serve modern JavaScript](https://web.dev/articles/codelab-serve-modern-code)',
43
+ /** Warning text that an outdated version of the library "core-js" was found, and the developer should upgrade. */
44
+ // eslint-disable-next-line max-len
45
+ detectedCoreJs2Warning: 'Version 2 of core-js was detected on the page. You should upgrade to version 3 for many performance improvements.',
43
46
  };
44
47
 
45
48
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
@@ -231,7 +234,6 @@ class LegacyJavascript extends ByteEfficiencyAudit {
231
234
  ['Reflect.preventExtensions', 'es6.reflect.prevent-extensions'],
232
235
  ['Reflect.setPrototypeOf', 'es6.reflect.set-prototype-of'],
233
236
  ['String.prototype.codePointAt', 'es6.string.code-point-at'],
234
- ['String.fromCodePoint', 'es6.string.from-code-point'],
235
237
  ['String.raw', 'es6.string.raw'],
236
238
  ['String.prototype.repeat', 'es6.string.repeat'],
237
239
  ['Object.entries', 'es7.object.entries'],
@@ -321,11 +323,10 @@ class LegacyJavascript extends ByteEfficiencyAudit {
321
323
  *
322
324
  * @param {CodePatternMatcher} matcher
323
325
  * @param {LH.Artifacts['Scripts']} scripts
324
- * @param {LH.Artifacts.NetworkRequest[]} networkRecords
325
326
  * @param {LH.Artifacts.Bundle[]} bundles
326
327
  * @return {Map<LH.Artifacts.Script, PatternMatchResult[]>}
327
328
  */
328
- static detectAcrossScripts(matcher, scripts, networkRecords, bundles) {
329
+ static detectAcrossScripts(matcher, scripts, bundles) {
329
330
  /** @type {Map<LH.Artifacts.Script, PatternMatchResult[]>} */
330
331
  const scriptToMatchResults = new Map();
331
332
  const polyfillData = this.getPolyfillData();
@@ -426,7 +427,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
426
427
  const compressionRatioByUrl = new Map();
427
428
 
428
429
  const scriptToMatchResults =
429
- this.detectAcrossScripts(matcher, artifacts.Scripts, networkRecords, bundles);
430
+ this.detectAcrossScripts(matcher, artifacts.Scripts, bundles);
430
431
  for (const [script, matches] of scriptToMatchResults.entries()) {
431
432
  const compressionRatio = estimateCompressionRatioForContent(
432
433
  compressionRatioByUrl, script.url, artifacts, networkRecords);
@@ -456,6 +457,16 @@ class LegacyJavascript extends ByteEfficiencyAudit {
456
457
  items.push(item);
457
458
  }
458
459
 
460
+ const warnings = [];
461
+ for (const bundle of bundles) {
462
+ if (classifiedEntities.isFirstParty(bundle.script.url)) {
463
+ if (bundle.rawMap.sources.some(s => s.match(/node_modules\/core-js\/modules\/es[67]/))) {
464
+ warnings.push(str_(UIStrings.detectedCoreJs2Warning));
465
+ break;
466
+ }
467
+ }
468
+ }
469
+
459
470
  /** @type {Map<string, number>} */
460
471
  const wastedBytesByUrl = new Map();
461
472
  for (const item of items) {
@@ -478,6 +489,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
478
489
  items,
479
490
  headings,
480
491
  wastedBytesByUrl,
492
+ warnings,
481
493
  };
482
494
  }
483
495
  }
@@ -1,54 +1,53 @@
1
1
  {
2
- "moduleSizes": [11897, 498, 265, 277, 263, 453, 219, 216, 546, 339, 1608, 671, 1525, 420, 214, 504, 98, 524, 196, 268, 642, 204, 742, 618, 169, 394, 127, 433, 1473, 779, 239, 144, 182, 254, 77, 508, 124, 1388, 75, 133, 301, 362, 170, 1078, 182, 490, 195, 321, 316, 447, 551, 216, 284, 253, 17, 107, 295, 356, 345, 1939, 1596, 291, 139, 259, 1291, 179, 528, 174, 61, 326, 20, 444, 522, 104, 1945, 120, 1943, 680, 1409, 850, 630, 288, 38, 695, 569, 106, 587, 208, 370, 606, 766, 535, 616, 200, 170, 224, 422, 970, 978, 498, 284, 241, 210, 151, 194, 178, 814, 205, 189, 215, 111, 236, 147, 237, 191, 691, 212, 432, 499, 445, 176, 333, 129, 414, 617, 380, 251, 199, 524, 515, 681, 160, 259, 295, 283, 178, 472, 786, 520, 202, 575, 575, 349, 549, 458, 166, 173, 508, 1522, 743, 414, 431, 393, 899, 137, 270, 131, 472, 457, 205, 778, 801, 133, 3000],
2
+ "moduleSizes": [17302, 498, 282, 294, 281, 467, 236, 229, 546, 339, 1608, 723, 1545, 438, 214, 111, 537, 209, 281, 685, 217, 757, 631, 182, 407, 140, 366, 1478, 359, 792, 269, 158, 280, 137, 189, 543, 1436, 88, 146, 314, 375, 183, 1083, 195, 503, 269, 208, 334, 350, 460, 568, 229, 334, 266, 30, 120, 309, 370, 358, 1952, 1638, 304, 153, 274, 1288, 192, 543, 187, 74, 144, 137, 33, 457, 535, 117, 1961, 133, 1956, 693, 1426, 863, 637, 301, 51, 708, 583, 119, 600, 221, 370, 728, 1085, 552, 629, 217, 183, 546, 137, 983, 992, 503, 402, 254, 223, 164, 214, 191, 831, 218, 202, 232, 124, 249, 160, 251, 217, 717, 225, 432, 499, 445, 177, 346, 142, 414, 617, 380, 264, 212, 524, 529, 708, 173, 272, 308, 296, 191, 485, 799, 533, 215, 589, 589, 362, 562, 471, 179, 186, 521, 1536, 756, 427, 444, 406, 912, 150, 283, 144, 485, 470, 205, 814, 146, 3000],
3
3
  "dependencies": {
4
- "Array.prototype.fill": [0, 5, 8, 11, 21, 23, 25, 26, 29, 36, 37, 54, 55, 57, 58, 60, 66, 73, 74, 75, 76, 77, 79, 81, 82, 86, 87, 88, 92, 94, 101, 102, 103, 104, 114, 116],
5
- "Array.prototype.filter": [0, 11, 12, 13, 17, 18, 21, 22, 23, 25, 26, 29, 36, 37, 41, 46, 54, 57, 58, 60, 62, 64, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 108, 114, 117],
6
- "Array.prototype.find": [0, 5, 11, 12, 17, 18, 21, 22, 23, 25, 26, 29, 36, 37, 41, 46, 54, 55, 57, 58, 60, 62, 64, 66, 73, 74, 75, 76, 77, 79, 81, 82, 86, 87, 88, 92, 94, 101, 102, 103, 104, 108, 114, 119],
7
- "Array.prototype.findIndex": [0, 5, 11, 12, 17, 18, 21, 22, 23, 25, 26, 29, 36, 37, 41, 46, 54, 55, 57, 58, 60, 62, 64, 66, 73, 74, 75, 76, 77, 79, 81, 82, 86, 87, 88, 92, 94, 101, 102, 103, 104, 108, 114, 118],
8
- "Array.prototype.forEach": [0, 9, 11, 12, 14, 17, 18, 21, 22, 23, 25, 26, 29, 36, 37, 41, 46, 54, 57, 58, 60, 62, 64, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 108, 114, 120],
9
- "Array.from": [0, 10, 11, 19, 20, 21, 22, 23, 25, 26, 27, 29, 36, 37, 41, 46, 49, 50, 54, 57, 58, 60, 61, 64, 66, 72, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 108, 114, 121],
10
- "Array.isArray": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 62, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 122],
11
- "Array.prototype.map": [0, 11, 12, 13, 17, 18, 21, 22, 23, 25, 26, 29, 36, 37, 41, 46, 54, 57, 58, 60, 62, 64, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 108, 114, 123],
12
- "Array.of": [0, 11, 21, 22, 23, 25, 26, 27, 29, 36, 37, 54, 57, 58, 60, 64, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 108, 114, 124],
13
- "Array.prototype.some": [0, 11, 12, 14, 17, 18, 21, 22, 23, 25, 26, 29, 36, 37, 41, 46, 54, 57, 58, 60, 62, 64, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 108, 114, 125],
14
- "Date.now": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 126],
15
- "Date.prototype.toISOString": [0, 11, 21, 22, 23, 25, 26, 28, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 98, 99, 101, 102, 103, 104, 108, 109, 114, 127],
16
- "Date.prototype.toJSON": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 128],
17
- "Date.prototype.toString": [0, 25, 26, 29, 54, 58, 60, 74, 94, 114, 129],
18
- "Function.prototype.name": [0, 130],
19
- "Number.isInteger": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 67, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 131],
20
- "Number.isSafeInteger": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 67, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 132],
21
- "Object.defineProperties": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 77, 79, 81, 82, 86, 87, 88, 92, 94, 101, 102, 103, 104, 114, 133],
22
- "Object.defineProperty": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 134],
23
- "Object.freeze": [0, 7, 11, 15, 21, 23, 25, 26, 27, 29, 36, 37, 39, 54, 57, 58, 59, 60, 66, 73, 74, 75, 79, 80, 81, 82, 84, 86, 88, 92, 94, 101, 102, 103, 104, 114, 136],
24
- "Object.getPrototypeOf": [0, 11, 21, 23, 24, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 83, 86, 88, 92, 94, 101, 102, 103, 104, 114, 138],
25
- "Object.isExtensible": [0, 7, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 84, 86, 88, 92, 94, 101, 102, 103, 104, 114, 139],
26
- "Object.isFrozen": [0, 7, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 140],
27
- "Object.isSealed": [0, 7, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 141],
28
- "Object.keys": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 87, 88, 92, 94, 101, 102, 103, 104, 114, 142],
29
- "Object.preventExtensions": [0, 7, 11, 15, 21, 23, 25, 26, 27, 29, 36, 37, 39, 54, 57, 58, 59, 60, 66, 73, 74, 75, 79, 80, 81, 82, 84, 86, 88, 92, 94, 101, 102, 103, 104, 114, 143],
30
- "Object.seal": [0, 7, 11, 15, 21, 23, 25, 26, 27, 29, 36, 37, 39, 54, 57, 58, 59, 60, 66, 73, 74, 75, 79, 80, 81, 82, 84, 86, 88, 92, 94, 101, 102, 103, 104, 114, 144],
31
- "Object.setPrototypeOf": [0, 4, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 89, 92, 94, 101, 102, 103, 104, 114, 145],
32
- "Reflect.apply": [0, 11, 21, 23, 25, 26, 29, 36, 37, 40, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 147],
33
- "Reflect.construct": [0, 3, 11, 16, 21, 22, 23, 25, 26, 29, 36, 37, 40, 43, 54, 55, 57, 58, 60, 64, 66, 73, 74, 75, 76, 77, 79, 81, 82, 86, 87, 88, 92, 94, 101, 102, 103, 104, 108, 114, 148],
34
- "Reflect.defineProperty": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 149],
35
- "Reflect.deleteProperty": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 150],
36
- "Reflect.get": [0, 11, 21, 23, 24, 25, 26, 29, 36, 37, 54, 57, 58, 60, 65, 66, 73, 74, 75, 79, 81, 82, 83, 86, 88, 92, 94, 101, 102, 103, 104, 114, 153],
37
- "Reflect.getOwnPropertyDescriptor": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 151],
38
- "Reflect.getPrototypeOf": [0, 11, 21, 23, 24, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 83, 86, 88, 92, 94, 101, 102, 103, 104, 114, 152],
39
- "Reflect.has": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 154],
40
- "Reflect.isExtensible": [0, 7, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 84, 86, 88, 92, 94, 101, 102, 103, 104, 114, 155],
41
- "Reflect.ownKeys": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 156],
42
- "Reflect.preventExtensions": [0, 11, 21, 23, 25, 26, 29, 36, 37, 39, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 157],
43
- "Reflect.setPrototypeOf": [0, 4, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 89, 92, 94, 101, 102, 103, 104, 114, 158],
44
- "String.prototype.codePointAt": [0, 11, 21, 22, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 97, 101, 102, 103, 104, 108, 109, 114, 159],
45
- "String.fromCodePoint": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 160],
46
- "String.raw": [0, 11, 21, 22, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 108, 109, 114, 161],
47
- "String.prototype.repeat": [0, 11, 21, 22, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 99, 101, 102, 103, 104, 108, 109, 114, 162],
48
- "Object.entries": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 87, 88, 90, 92, 94, 101, 102, 103, 104, 114, 135],
49
- "Object.getOwnPropertyDescriptors": [0, 11, 21, 23, 25, 26, 27, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 88, 92, 94, 101, 102, 103, 104, 114, 137],
50
- "Object.values": [0, 11, 21, 23, 25, 26, 29, 36, 37, 54, 57, 58, 60, 66, 73, 74, 75, 79, 81, 82, 86, 87, 88, 90, 92, 94, 101, 102, 103, 104, 114, 146],
4
+ "Array.prototype.fill": [0, 5, 8, 11, 20, 22, 29, 33, 36, 55, 57, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 117],
5
+ "Array.prototype.filter": [0, 11, 12, 13, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 118],
6
+ "Array.prototype.find": [0, 5, 11, 12, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 55, 57, 62, 64, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 109, 120],
7
+ "Array.prototype.findIndex": [0, 5, 11, 12, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 55, 57, 62, 64, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 109, 119],
8
+ "Array.prototype.forEach": [0, 9, 11, 12, 14, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 121],
9
+ "Array.from": [0, 10, 11, 18, 19, 20, 21, 22, 26, 29, 33, 36, 40, 46, 49, 50, 57, 61, 64, 66, 73, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 122],
10
+ "Array.isArray": [0, 11, 20, 22, 29, 33, 36, 57, 62, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 123],
11
+ "Array.prototype.map": [0, 11, 12, 13, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 124],
12
+ "Array.of": [0, 11, 20, 21, 22, 26, 29, 33, 36, 57, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 125],
13
+ "Array.prototype.some": [0, 11, 12, 14, 16, 17, 20, 21, 22, 29, 33, 36, 40, 46, 57, 62, 64, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 126],
14
+ "Date.now": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 127],
15
+ "Date.prototype.toISOString": [0, 11, 20, 21, 22, 27, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 99, 100, 102, 103, 104, 105, 109, 110, 128],
16
+ "Date.prototype.toJSON": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 129],
17
+ "Date.prototype.toString": [0, 29, 130],
18
+ "Function.prototype.name": [0, 28, 131],
19
+ "Number.isInteger": [0, 11, 20, 22, 29, 33, 36, 57, 66, 67, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 132],
20
+ "Number.isSafeInteger": [0, 11, 20, 22, 29, 33, 36, 57, 66, 67, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 133],
21
+ "Object.defineProperties": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 134],
22
+ "Object.defineProperty": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 135],
23
+ "Object.freeze": [0, 7, 11, 15, 20, 22, 29, 33, 36, 38, 57, 59, 66, 74, 76, 80, 81, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 137],
24
+ "Object.getPrototypeOf": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 89, 93, 102, 103, 104, 105, 139],
25
+ "Object.isExtensible": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 140],
26
+ "Object.isFrozen": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 141],
27
+ "Object.isSealed": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 142],
28
+ "Object.keys": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 143],
29
+ "Object.preventExtensions": [0, 7, 11, 15, 20, 22, 29, 33, 36, 38, 57, 59, 66, 74, 76, 80, 81, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 144],
30
+ "Object.seal": [0, 7, 11, 15, 20, 22, 29, 33, 36, 38, 57, 59, 66, 74, 76, 80, 81, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 145],
31
+ "Object.setPrototypeOf": [0, 4, 11, 20, 22, 29, 33, 36, 45, 57, 66, 70, 74, 76, 80, 82, 83, 87, 89, 90, 93, 102, 103, 104, 105, 146],
32
+ "Reflect.apply": [0, 11, 20, 22, 29, 33, 36, 39, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 148],
33
+ "Reflect.construct": [0, 3, 11, 15, 20, 21, 22, 29, 33, 36, 39, 42, 55, 57, 64, 66, 74, 76, 77, 78, 80, 82, 83, 87, 88, 89, 93, 102, 103, 104, 105, 109, 149],
34
+ "Reflect.defineProperty": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 150],
35
+ "Reflect.deleteProperty": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 151],
36
+ "Reflect.get": [0, 11, 20, 22, 23, 29, 33, 36, 57, 65, 66, 74, 76, 80, 82, 83, 84, 87, 89, 93, 102, 103, 104, 105, 154],
37
+ "Reflect.getOwnPropertyDescriptor": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 152],
38
+ "Reflect.getPrototypeOf": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 89, 93, 102, 103, 104, 105, 153],
39
+ "Reflect.has": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 155],
40
+ "Reflect.isExtensible": [0, 7, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 85, 87, 89, 93, 102, 103, 104, 105, 156],
41
+ "Reflect.ownKeys": [0, 11, 20, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 157],
42
+ "Reflect.preventExtensions": [0, 11, 20, 22, 29, 33, 36, 38, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 158],
43
+ "Reflect.setPrototypeOf": [0, 4, 11, 20, 22, 29, 33, 36, 45, 57, 66, 70, 74, 76, 80, 82, 83, 87, 89, 90, 93, 102, 103, 104, 105, 159],
44
+ "String.prototype.codePointAt": [0, 11, 20, 21, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 98, 102, 103, 104, 105, 109, 110, 160],
45
+ "String.raw": [0, 11, 20, 21, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 109, 110, 161],
46
+ "String.prototype.repeat": [0, 11, 20, 21, 22, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 100, 102, 103, 104, 105, 109, 110, 162],
47
+ "Object.entries": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 88, 89, 91, 93, 102, 103, 104, 105, 136],
48
+ "Object.getOwnPropertyDescriptors": [0, 11, 20, 22, 26, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 87, 89, 93, 102, 103, 104, 105, 138],
49
+ "Object.values": [0, 11, 20, 22, 23, 29, 33, 36, 57, 66, 74, 76, 80, 82, 83, 84, 87, 88, 89, 91, 93, 102, 103, 104, 105, 147],
51
50
  "focus-visible": [163]
52
51
  },
53
- "maxSize": 87683
52
+ "maxSize": 94935
54
53
  }
@@ -8,7 +8,6 @@ import {Audit} from '../audit.js';
8
8
  import * as i18n from '../../lib/i18n/i18n.js';
9
9
  import {NetworkRequest} from '../../lib/network-request.js';
10
10
  import {NetworkRecords} from '../../computed/network-records.js';
11
- import {Util} from '../../../shared/util.js';
12
11
 
13
12
  const UIStrings = {
14
13
  /** Title of a diagnostic audit that provides detail on large network resources required during page load. 'Payloads' is roughly equivalent to 'resources'. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -99,7 +98,6 @@ class TotalByteWeight extends Audit {
99
98
 
100
99
  return {
101
100
  score,
102
- scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
103
101
  numericValue: totalBytes,
104
102
  numericUnit: 'byte',
105
103
  displayValue: str_(UIStrings.displayValue, {totalBytes}),
@@ -14,7 +14,6 @@
14
14
  import {Audit} from '../audit.js';
15
15
  import * as i18n from '../../lib/i18n/i18n.js';
16
16
  import {TBTImpactTasks} from '../../computed/tbt-impact-tasks.js';
17
- import {Util} from '../../../shared/util.js';
18
17
 
19
18
  const UIStrings = {
20
19
  /** Title of a diagnostic audit that provides detail on the size of the web page's DOM. The size of a DOM is characterized by the total number of DOM elements and greatest DOM depth. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -168,7 +167,6 @@ class DOMSize extends Audit {
168
167
 
169
168
  return {
170
169
  score,
171
- scoreDisplayMode: score >= Util.PASS_THRESHOLD ? Audit.SCORING_MODES.INFORMATIVE : undefined,
172
170
  numericValue: stats.totalBodyElements,
173
171
  numericUnit: 'element',
174
172
  displayValue: str_(UIStrings.displayValue, {itemCount: stats.totalBodyElements}),
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-unused-vars */ // TODO: remove once implemented.
2
-
3
1
  /**
4
2
  * @license
5
3
  * Copyright 2025 Google LLC
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/ImageDeli
10
8
 
11
9
  import {Audit} from '../audit.js';
12
10
  import * as i18n from '../../lib/i18n/i18n.js';
13
- import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
11
+ import {adaptInsightToAuditProduct} from './insight-audit.js';
14
12
 
15
13
  // eslint-disable-next-line max-len
16
14
  const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/ImageDelivery.js', UIStrings);
@@ -36,14 +34,41 @@ class ImageDeliveryInsight extends Audit {
36
34
  * @return {Promise<LH.Audit.Product>}
37
35
  */
38
36
  static async audit(artifacts, context) {
39
- // TODO: implement.
40
37
  return adaptInsightToAuditProduct(artifacts, context, 'ImageDelivery', (insight) => {
38
+ if (!insight.optimizableImages.length) {
39
+ // TODO: show UIStrings.noOptimizableImages?
40
+ return;
41
+ }
42
+
43
+ const relatedEventsMap = insight.relatedEvents && !Array.isArray(insight.relatedEvents) ?
44
+ insight.relatedEvents :
45
+ null;
46
+
41
47
  /** @type {LH.Audit.Details.Table['headings']} */
42
48
  const headings = [
49
+ /* eslint-disable max-len */
50
+ {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL), subItemsHeading: {key: 'reason', valueType: 'text'}},
51
+ {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnResourceSize)},
52
+ {key: 'wastedBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnWastedBytes), subItemsHeading: {key: 'wastedBytes', valueType: 'bytes'}},
53
+ /* eslint-enable max-len */
43
54
  ];
55
+
44
56
  /** @type {LH.Audit.Details.Table['items']} */
45
- const items = [
46
- ];
57
+ const items = insight.optimizableImages.map(image => ({
58
+ url: image.request.args.data.url,
59
+ totalBytes: image.request.args.data.decodedBodyLength,
60
+ wastedBytes: image.byteSavings,
61
+ subItems: {
62
+ type: /** @type {const} */ ('subitems'),
63
+ // TODO: when strings update to remove number from "reason" uistrings, update this
64
+ // to use `image.optimizations.map(...)` and construct strings from the type.
65
+ items: (relatedEventsMap?.get(image.request) ?? []).map((reason, i) => ({
66
+ reason,
67
+ wastedBytes: image.optimizations[i].byteSavings,
68
+ })),
69
+ },
70
+ }));
71
+
47
72
  return Audit.makeTableDetails(headings, items);
48
73
  });
49
74
  }
@@ -13,4 +13,11 @@ export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/tr
13
13
  * @return {LH.Audit.Details.NodeValue|undefined}
14
14
  */
15
15
  export function makeNodeItemForNodeId(traceElements: LH.Artifacts.TraceElement[], nodeId: number | null | undefined): LH.Audit.Details.NodeValue | undefined;
16
+ /**
17
+ * @param {LH.Artifacts.TraceElement[]} traceElements
18
+ * @param {number|null|undefined} nodeId
19
+ * @param {LH.IcuMessage|string} label
20
+ * @return {LH.Audit.Details.Table|undefined}
21
+ */
22
+ export function maybeMakeNodeElementTable(traceElements: LH.Artifacts.TraceElement[], nodeId: number | null | undefined, label: LH.IcuMessage | string): LH.Audit.Details.Table | undefined;
16
23
  //# sourceMappingURL=insight-audit.d.ts.map
@@ -44,6 +44,14 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
44
44
  }
45
45
 
46
46
  const insight = insights.model[insightName];
47
+ if (insight instanceof Error) {
48
+ return {
49
+ errorMessage: insight.message,
50
+ errorStack: insight.stack,
51
+ score: null,
52
+ };
53
+ }
54
+
47
55
  const details = createDetails(insight);
48
56
  if (!details || (details.type === 'table' && details.headings.length === 0)) {
49
57
  return {
@@ -63,10 +71,18 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
63
71
  metricSavings = {...metricSavings, LCP: /** @type {any} */ (0)};
64
72
  }
65
73
 
74
+ let score = insight.shouldShow ? 0 : 1;
75
+ // TODO: change insight model to denote passing/failing/informative. Until then... hack it.
76
+ if (insightName === 'LCPPhases') {
77
+ score = metricSavings?.LCP ?? 0 >= 1000 ? 0 : 1;
78
+ } else if (insightName === 'InteractionToNextPaint') {
79
+ score = metricSavings?.INP ?? 0 >= 500 ? 0 : 1;
80
+ }
81
+
66
82
  return {
67
83
  scoreDisplayMode:
68
84
  insight.metricSavings ? Audit.SCORING_MODES.METRIC_SAVINGS : Audit.SCORING_MODES.NUMERIC,
69
- score: insight.shouldShow ? 0 : 1,
85
+ score,
70
86
  metricSavings,
71
87
  warnings: insight.warnings,
72
88
  details,
@@ -93,7 +109,25 @@ function makeNodeItemForNodeId(traceElements, nodeId) {
93
109
  return Audit.makeNodeItem(node);
94
110
  }
95
111
 
112
+ /**
113
+ * @param {LH.Artifacts.TraceElement[]} traceElements
114
+ * @param {number|null|undefined} nodeId
115
+ * @param {LH.IcuMessage|string} label
116
+ * @return {LH.Audit.Details.Table|undefined}
117
+ */
118
+ function maybeMakeNodeElementTable(traceElements, nodeId, label) {
119
+ const node = makeNodeItemForNodeId(traceElements, nodeId);
120
+ if (!node) {
121
+ return;
122
+ }
123
+
124
+ return Audit.makeTableDetails([
125
+ {key: 'node', valueType: 'node', label},
126
+ ], [{node}]);
127
+ }
128
+
96
129
  export {
97
130
  adaptInsightToAuditProduct,
98
131
  makeNodeItemForNodeId,
132
+ maybeMakeNodeElementTable,
99
133
  };
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-unused-vars */ // TODO: remove once implemented.
2
-
3
1
  /**
4
2
  * @license
5
3
  * Copyright 2025 Google LLC
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/Interacti
10
8
 
11
9
  import {Audit} from '../audit.js';
12
10
  import * as i18n from '../../lib/i18n/i18n.js';
13
- import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
11
+ import {adaptInsightToAuditProduct, maybeMakeNodeElementTable} from './insight-audit.js';
14
12
 
15
13
  // eslint-disable-next-line max-len
16
14
  const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/InteractionToNextPaint.js', UIStrings);
@@ -36,15 +34,35 @@ class InteractionToNextPaintInsight extends Audit {
36
34
  * @return {Promise<LH.Audit.Product>}
37
35
  */
38
36
  static async audit(artifacts, context) {
39
- // TODO: implement.
40
37
  return adaptInsightToAuditProduct(artifacts, context, 'InteractionToNextPaint', (insight) => {
38
+ const event = insight.longestInteractionEvent;
39
+ if (!event) {
40
+ // TODO: show UIStrings.noInteractions?
41
+ return;
42
+ }
43
+
41
44
  /** @type {LH.Audit.Details.Table['headings']} */
42
45
  const headings = [
46
+ {key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
47
+ {key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
43
48
  ];
49
+
44
50
  /** @type {LH.Audit.Details.Table['items']} */
45
51
  const items = [
52
+ /* eslint-disable max-len */
53
+ {phase: 'inputDelay', label: str_(UIStrings.inputDelay), duration: event.inputDelay / 1000},
54
+ {phase: 'processingDuration', label: str_(UIStrings.processingDuration), duration: event.mainThreadHandling / 1000},
55
+ {phase: 'presentationDelay', label: str_(UIStrings.presentationDelay), duration: event.presentationDelay / 1000},
56
+ /* eslint-enable max-len */
46
57
  ];
47
- return Audit.makeTableDetails(headings, items);
58
+
59
+ return Audit.makeListDetails([
60
+ maybeMakeNodeElementTable(
61
+ artifacts.TraceElements,
62
+ event.args.data.beginEvent.args.data.nodeId,
63
+ str_(i18n.UIStrings.columnElement)),
64
+ Audit.makeTableDetails(headings, items),
65
+ ].filter(table => !!table));
48
66
  });
49
67
  }
50
68
  }
@@ -1,5 +1,10 @@
1
1
  export default LCPPhasesInsight;
2
2
  declare class LCPPhasesInsight extends Audit {
3
+ /**
4
+ * @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPPhases.js').LCPPhasesInsightModel>['phases']} phases
5
+ * @return {LH.Audit.Details.Table}
6
+ */
7
+ static makePhaseTable(phases: Required<import("@paulirish/trace_engine/models/trace/insights/LCPPhases.js").LCPPhasesInsightModel>["phases"]): LH.Audit.Details.Table;
3
8
  /**
4
9
  * @param {LH.Artifacts} artifacts
5
10
  * @param {LH.Audit.Context} context
@@ -1,5 +1,3 @@
1
- /* eslint-disable no-unused-vars */ // TODO: remove once implemented.
2
-
3
1
  /**
4
2
  * @license
5
3
  * Copyright 2025 Google LLC
@@ -10,7 +8,7 @@ import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/LCPPhases
10
8
 
11
9
  import {Audit} from '../audit.js';
12
10
  import * as i18n from '../../lib/i18n/i18n.js';
13
- import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
11
+ import {adaptInsightToAuditProduct, maybeMakeNodeElementTable} from './insight-audit.js';
14
12
 
15
13
  // eslint-disable-next-line max-len
16
14
  const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/LCPPhases.js', UIStrings);
@@ -30,21 +28,57 @@ class LCPPhasesInsight extends Audit {
30
28
  };
31
29
  }
32
30
 
31
+ /**
32
+ * @param {Required<import('@paulirish/trace_engine/models/trace/insights/LCPPhases.js').LCPPhasesInsightModel>['phases']} phases
33
+ * @return {LH.Audit.Details.Table}
34
+ */
35
+ static makePhaseTable(phases) {
36
+ const {ttfb, loadDelay, loadTime, renderDelay} = phases;
37
+
38
+ /** @type {LH.Audit.Details.Table['headings']} */
39
+ const headings = [
40
+ {key: 'label', valueType: 'text', label: str_(UIStrings.phase)},
41
+ {key: 'duration', valueType: 'ms', label: str_(i18n.UIStrings.columnDuration)},
42
+ ];
43
+
44
+ /** @type {LH.Audit.Details.Table['items']} */
45
+ let items = [
46
+ /* eslint-disable max-len */
47
+ {phase: 'timeToFirstByte', label: str_(UIStrings.timeToFirstByte), duration: ttfb},
48
+ {phase: 'resourceLoadDelay', label: str_(UIStrings.resourceLoadDelay), duration: loadDelay},
49
+ {phase: 'resourceLoadDuration', label: str_(UIStrings.resourceLoadDuration), duration: loadTime},
50
+ {phase: 'elementRenderDelay', label: str_(UIStrings.elementRenderDelay), duration: renderDelay},
51
+ /* eslint-enable max-len */
52
+ ];
53
+
54
+ if (loadDelay === undefined) {
55
+ items = items.filter(item => item.phase !== 'resourceLoadDelay');
56
+ }
57
+ if (loadTime === undefined) {
58
+ items = items.filter(item => item.phase !== 'resourceLoadDuration');
59
+ }
60
+
61
+ return Audit.makeTableDetails(headings, items);
62
+ }
63
+
33
64
  /**
34
65
  * @param {LH.Artifacts} artifacts
35
66
  * @param {LH.Audit.Context} context
36
67
  * @return {Promise<LH.Audit.Product>}
37
68
  */
38
69
  static async audit(artifacts, context) {
39
- // TODO: implement.
40
70
  return adaptInsightToAuditProduct(artifacts, context, 'LCPPhases', (insight) => {
41
- /** @type {LH.Audit.Details.Table['headings']} */
42
- const headings = [
43
- ];
44
- /** @type {LH.Audit.Details.Table['items']} */
45
- const items = [
46
- ];
47
- return Audit.makeTableDetails(headings, items);
71
+ if (!insight.phases) {
72
+ return;
73
+ }
74
+
75
+ return Audit.makeListDetails([
76
+ maybeMakeNodeElementTable(
77
+ artifacts.TraceElements,
78
+ insight.lcpEvent?.args.data?.nodeId,
79
+ str_(i18n.UIStrings.columnElement)),
80
+ LCPPhasesInsight.makePhaseTable(insight.phases),
81
+ ].filter(table => table !== undefined));
48
82
  });
49
83
  }
50
84
  }