lighthouse 12.8.2-dev.20251005 → 12.8.2-dev.20251007

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 (142) hide show
  1. package/cli/test/smokehouse/config/exclusions.js +0 -2
  2. package/core/audits/audit.js +0 -1
  3. package/core/audits/insights/cls-culprits-insight.js +1 -1
  4. package/core/audits/insights/dom-size-insight.js +11 -7
  5. package/core/audits/insights/insight-audit.d.ts +4 -2
  6. package/core/audits/insights/insight-audit.js +22 -3
  7. package/core/audits/predictive-perf.js +2 -2
  8. package/core/audits/seo/crawlable-anchors.js +2 -3
  9. package/core/audits/server-response-time.d.ts +0 -5
  10. package/core/audits/server-response-time.js +12 -26
  11. package/core/computed/metrics/lcp-breakdown.d.ts +10 -5
  12. package/core/computed/metrics/lcp-breakdown.js +50 -22
  13. package/core/computed/metrics/time-to-first-byte.js +33 -10
  14. package/core/computed/metrics/timing-summary.js +3 -2
  15. package/core/config/default-config.js +20 -63
  16. package/core/config/experimental-config.js +1 -26
  17. package/core/config/filters.js +6 -9
  18. package/core/config/lr-desktop-config.js +0 -1
  19. package/core/config/lr-mobile-config.js +0 -1
  20. package/core/gather/gatherers/anchor-elements.js +8 -24
  21. package/core/gather/gatherers/inspector-issues.js +1 -28
  22. package/core/gather/gatherers/trace-elements.d.ts +0 -9
  23. package/core/gather/gatherers/trace-elements.js +0 -35
  24. package/core/lib/network-request.d.ts +0 -7
  25. package/core/lib/network-request.js +0 -16
  26. package/core/lib/proto-preprocessor.js +5 -22
  27. package/dist/report/bundle.esm.js +10 -49
  28. package/dist/report/flow.js +12 -51
  29. package/dist/report/standalone.js +11 -50
  30. package/flow-report/src/i18n/i18n.d.ts +4 -6
  31. package/package.json +4 -5
  32. package/report/assets/styles.css +0 -39
  33. package/report/renderer/api.js +0 -1
  34. package/report/renderer/category-renderer.js +6 -0
  35. package/report/renderer/components.js +1 -1
  36. package/report/renderer/dom.d.ts +0 -13
  37. package/report/renderer/dom.js +0 -38
  38. package/report/renderer/performance-category-renderer.d.ts +0 -26
  39. package/report/renderer/performance-category-renderer.js +10 -142
  40. package/report/renderer/report-ui-features.d.ts +0 -1
  41. package/report/renderer/report-ui-features.js +3 -13
  42. package/report/renderer/report-utils.d.ts +2 -3
  43. package/report/renderer/report-utils.js +4 -6
  44. package/report/types/report-renderer.d.ts +0 -6
  45. package/shared/localization/locales/ar-XB.json +20 -341
  46. package/shared/localization/locales/ar.json +20 -341
  47. package/shared/localization/locales/bg.json +9 -330
  48. package/shared/localization/locales/ca.json +9 -330
  49. package/shared/localization/locales/cs.json +9 -330
  50. package/shared/localization/locales/da.json +9 -330
  51. package/shared/localization/locales/de.json +9 -330
  52. package/shared/localization/locales/el.json +9 -330
  53. package/shared/localization/locales/en-GB.json +9 -330
  54. package/shared/localization/locales/en-US.json +44 -293
  55. package/shared/localization/locales/en-XA.json +0 -330
  56. package/shared/localization/locales/en-XL.json +44 -293
  57. package/shared/localization/locales/es-419.json +9 -330
  58. package/shared/localization/locales/es.json +9 -330
  59. package/shared/localization/locales/fi.json +9 -330
  60. package/shared/localization/locales/fil.json +9 -330
  61. package/shared/localization/locales/fr.json +9 -330
  62. package/shared/localization/locales/he.json +31 -352
  63. package/shared/localization/locales/hi.json +9 -330
  64. package/shared/localization/locales/hr.json +9 -330
  65. package/shared/localization/locales/hu.json +9 -330
  66. package/shared/localization/locales/id.json +9 -330
  67. package/shared/localization/locales/it.json +9 -330
  68. package/shared/localization/locales/ja.json +9 -330
  69. package/shared/localization/locales/ko.json +10 -331
  70. package/shared/localization/locales/lt.json +9 -330
  71. package/shared/localization/locales/lv.json +10 -331
  72. package/shared/localization/locales/nl.json +9 -330
  73. package/shared/localization/locales/no.json +9 -330
  74. package/shared/localization/locales/pl.json +9 -330
  75. package/shared/localization/locales/pt-PT.json +9 -330
  76. package/shared/localization/locales/pt.json +9 -330
  77. package/shared/localization/locales/ro.json +10 -331
  78. package/shared/localization/locales/ru.json +9 -330
  79. package/shared/localization/locales/sk.json +9 -330
  80. package/shared/localization/locales/sl.json +9 -330
  81. package/shared/localization/locales/sr-Latn.json +9 -330
  82. package/shared/localization/locales/sr.json +9 -330
  83. package/shared/localization/locales/sv.json +9 -330
  84. package/shared/localization/locales/ta.json +9 -330
  85. package/shared/localization/locales/te.json +10 -331
  86. package/shared/localization/locales/th.json +9 -330
  87. package/shared/localization/locales/tr.json +9 -330
  88. package/shared/localization/locales/uk.json +9 -330
  89. package/shared/localization/locales/vi.json +9 -330
  90. package/shared/localization/locales/zh-HK.json +9 -330
  91. package/shared/localization/locales/zh-TW.json +10 -331
  92. package/shared/localization/locales/zh.json +9 -330
  93. package/types/artifacts.d.ts +5 -6
  94. package/types/audit.d.ts +1 -1
  95. package/types/lhr/settings.d.ts +1 -1
  96. package/core/audits/byte-efficiency/duplicated-javascript.d.ts +0 -45
  97. package/core/audits/byte-efficiency/duplicated-javascript.js +0 -223
  98. package/core/audits/byte-efficiency/efficient-animated-content.d.ts +0 -22
  99. package/core/audits/byte-efficiency/efficient-animated-content.js +0 -93
  100. package/core/audits/byte-efficiency/legacy-javascript.d.ts +0 -28
  101. package/core/audits/byte-efficiency/legacy-javascript.js +0 -144
  102. package/core/audits/byte-efficiency/modern-image-formats.d.ts +0 -38
  103. package/core/audits/byte-efficiency/modern-image-formats.js +0 -187
  104. package/core/audits/byte-efficiency/render-blocking-resources.d.ts +0 -53
  105. package/core/audits/byte-efficiency/render-blocking-resources.js +0 -312
  106. package/core/audits/byte-efficiency/uses-long-cache-ttl.d.ts +0 -59
  107. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +0 -293
  108. package/core/audits/byte-efficiency/uses-optimized-images.d.ts +0 -33
  109. package/core/audits/byte-efficiency/uses-optimized-images.js +0 -146
  110. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.d.ts +0 -16
  111. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +0 -106
  112. package/core/audits/byte-efficiency/uses-responsive-images.d.ts +0 -44
  113. package/core/audits/byte-efficiency/uses-responsive-images.js +0 -202
  114. package/core/audits/byte-efficiency/uses-text-compression.d.ts +0 -14
  115. package/core/audits/byte-efficiency/uses-text-compression.js +0 -108
  116. package/core/audits/critical-request-chains.d.ts +0 -44
  117. package/core/audits/critical-request-chains.js +0 -221
  118. package/core/audits/dobetterweb/dom-size.d.ts +0 -32
  119. package/core/audits/dobetterweb/dom-size.js +0 -182
  120. package/core/audits/dobetterweb/uses-http2.d.ts +0 -72
  121. package/core/audits/dobetterweb/uses-http2.js +0 -276
  122. package/core/audits/font-display.d.ts +0 -32
  123. package/core/audits/font-display.js +0 -195
  124. package/core/audits/largest-contentful-paint-element.d.ts +0 -34
  125. package/core/audits/largest-contentful-paint-element.js +0 -181
  126. package/core/audits/lcp-lazy-loaded.d.ts +0 -22
  127. package/core/audits/lcp-lazy-loaded.js +0 -115
  128. package/core/audits/prioritize-lcp-image.d.ts +0 -74
  129. package/core/audits/prioritize-lcp-image.js +0 -297
  130. package/core/audits/third-party-summary.d.ts +0 -78
  131. package/core/audits/third-party-summary.js +0 -236
  132. package/core/audits/uses-rel-preconnect.d.ts +0 -37
  133. package/core/audits/uses-rel-preconnect.js +0 -286
  134. package/core/audits/viewport.d.ts +0 -17
  135. package/core/audits/viewport.js +0 -87
  136. package/core/audits/work-during-interaction.d.ts +0 -81
  137. package/core/audits/work-during-interaction.js +0 -287
  138. package/core/computed/critical-request-chains.d.ts +0 -42
  139. package/core/computed/critical-request-chains.js +0 -143
  140. package/core/computed/viewport-meta.d.ts +0 -37
  141. package/core/computed/viewport-meta.js +0 -71
  142. package/types/internal/metaviewport-parser.d.ts +0 -13
@@ -36,8 +36,6 @@ const exclusions = {
36
36
  for (const array of Object.values(exclusions)) {
37
37
  // https://github.com/GoogleChrome/lighthouse/issues/14271
38
38
  array.push('lantern-idle-callback-short');
39
- // https://github.com/GoogleChrome/lighthouse/issues/16597
40
- array.push('csp-block-all');
41
39
  // glitch is gone.
42
40
  array.push('issues-mixed-content');
43
41
  // works most of the time, but since it uses a live site it can be flaky
@@ -484,7 +484,6 @@ class Audit {
484
484
 
485
485
  details: product.details,
486
486
  guidanceLevel: audit.meta.guidanceLevel,
487
- replacesAudits: audit.meta.replacesAudits,
488
487
  };
489
488
  }
490
489
 
@@ -38,7 +38,7 @@ class CLSCulpritsInsight extends Audit {
38
38
  description: insightStr_(InsightUIStrings.description),
39
39
  guidanceLevel: 3,
40
40
  requiredArtifacts: ['Trace', 'TraceElements', 'SourceMaps'],
41
- replacesAudits: ['layout-shifts', 'non-composited-animations', 'unsized-images'],
41
+ replacesAudits: ['layout-shifts'],
42
42
  };
43
43
  }
44
44
 
@@ -59,21 +59,21 @@ class DOMSizeInsight extends Audit {
59
59
  },
60
60
  },
61
61
  {
62
- statistic: str_(UIStrings.maxChildren),
63
- node: makeNodeItemForNodeId(artifacts.TraceElements, maxChildren.nodeId),
62
+ statistic: str_(UIStrings.maxDOMDepth),
63
+ node: makeNodeItemForNodeId(artifacts.TraceElements, maxDepth.nodeId),
64
64
  value: {
65
65
  type: 'numeric',
66
66
  granularity: 1,
67
- value: maxChildren.numChildren,
67
+ value: maxDepth.depth,
68
68
  },
69
69
  },
70
70
  {
71
- statistic: str_(UIStrings.maxDOMDepth),
72
- node: makeNodeItemForNodeId(artifacts.TraceElements, maxDepth.nodeId),
71
+ statistic: str_(UIStrings.maxChildren),
72
+ node: makeNodeItemForNodeId(artifacts.TraceElements, maxChildren.nodeId),
73
73
  value: {
74
74
  type: 'numeric',
75
75
  granularity: 1,
76
- value: maxDepth.depth,
76
+ value: maxChildren.numChildren,
77
77
  },
78
78
  },
79
79
  ];
@@ -85,7 +85,11 @@ class DOMSizeInsight extends Audit {
85
85
  maxChildren: maxChildren.numChildren,
86
86
  maxDepth: maxDepth.depth,
87
87
  };
88
- return details;
88
+ return {
89
+ details,
90
+ numericValue: totalElements,
91
+ numericUnit: 'element',
92
+ };
89
93
  });
90
94
  }
91
95
  }
@@ -11,13 +11,15 @@ export type CreateDetailsExtras = {
11
11
  * @param {LH.Artifacts} artifacts
12
12
  * @param {LH.Audit.Context} context
13
13
  * @param {T} insightName
14
- * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings: Array<string | LH.IcuMessage>}|LH.Audit.Details|undefined} createDetails
14
+ * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
15
15
  * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
16
16
  * @return {Promise<LH.Audit.Product>}
17
17
  */
18
18
  export function adaptInsightToAuditProduct<T extends keyof import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModelsType>(artifacts: LH.Artifacts, context: LH.Audit.Context, insightName: T, createDetails: (insight: import("@paulirish/trace_engine/models/trace/insights/types.js").InsightModels[T], extras: CreateDetailsExtras) => {
19
19
  details: LH.Audit.Details;
20
- warnings: Array<string | LH.IcuMessage>;
20
+ warnings?: Array<string | LH.IcuMessage>;
21
+ numericValue?: number;
22
+ numericUnit?: LH.Audit.NumericProduct["numericUnit"];
21
23
  } | LH.Audit.Details | undefined): Promise<LH.Audit.Product>;
22
24
  /**
23
25
  * @param {LH.Artifacts.TraceElement[]} traceElements
@@ -42,7 +42,7 @@ async function getInsightSet(artifacts, context) {
42
42
  * @param {LH.Artifacts} artifacts
43
43
  * @param {LH.Audit.Context} context
44
44
  * @param {T} insightName
45
- * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings: Array<string | LH.IcuMessage>}|LH.Audit.Details|undefined} createDetails
45
+ * @param {(insight: import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModels[T], extras: CreateDetailsExtras) => {details: LH.Audit.Details, warnings?: Array<string | LH.IcuMessage>, numericValue?: number, numericUnit?: LH.Audit.NumericProduct['numericUnit']}|LH.Audit.Details|undefined} createDetails
46
46
  * @template {keyof import('@paulirish/trace_engine/models/trace/insights/types.js').InsightModelsType} T
47
47
  * @return {Promise<LH.Audit.Product>}
48
48
  */
@@ -70,11 +70,17 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
70
70
  });
71
71
 
72
72
  const warnings = [...insight.warnings ?? []];
73
+ /** @type {number|undefined} */
74
+ let numericValue;
75
+ /** @type {LH.Audit.NumericProduct['numericUnit']|undefined} */
76
+ let numericUnit;
73
77
 
74
78
  let details;
75
- if (cbResult && 'warnings' in cbResult) {
79
+ if (cbResult && 'details' in cbResult) {
76
80
  details = cbResult.details;
77
- warnings.push(...cbResult.warnings);
81
+ if (cbResult.warnings) warnings.push(...cbResult.warnings);
82
+ numericValue = cbResult.numericValue;
83
+ numericUnit = cbResult.numericUnit;
78
84
  } else {
79
85
  details = cbResult;
80
86
  }
@@ -163,6 +169,19 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
163
169
  scoreDisplayMode = Audit.SCORING_MODES.INFORMATIVE;
164
170
  }
165
171
 
172
+ if (numericValue !== undefined && numericUnit !== undefined) {
173
+ return {
174
+ scoreDisplayMode,
175
+ score,
176
+ numericValue,
177
+ numericUnit,
178
+ metricSavings,
179
+ warnings: warnings.length ? warnings : undefined,
180
+ displayValue,
181
+ details,
182
+ };
183
+ }
184
+
166
185
  return {
167
186
  scoreDisplayMode,
168
187
  score,
@@ -76,8 +76,8 @@ class PredictivePerf extends Audit {
76
76
  pessimisticLCP: lcp.pessimisticEstimate.timeInMs,
77
77
 
78
78
  roughEstimateOfTTFB: timingSummary.metrics.timeToFirstByte,
79
- roughEstimateOfLCPLoadStart: timingSummary.metrics.lcpLoadStart,
80
- roughEstimateOfLCPLoadEnd: timingSummary.metrics.lcpLoadEnd,
79
+ roughEstimateOfLCPLoadStart: timingSummary.metrics.lcpLoadDelay,
80
+ roughEstimateOfLCPLoadEnd: timingSummary.metrics.lcpLoadDuration,
81
81
  };
82
82
 
83
83
  const score = Audit.computeLogNormalScore(
@@ -57,12 +57,10 @@ class CrawlableAnchors extends Audit {
57
57
  href,
58
58
  attributeNames = [],
59
59
  listeners = [],
60
- ancestorListeners = [],
61
60
  }) => {
62
61
  rawHref = rawHref.replace( /\s/g, '');
63
62
  name = name.trim();
64
63
  role = role.trim();
65
- const hasListener = Boolean(listeners.length || ancestorListeners.length);
66
64
 
67
65
  if (role.length > 0) return;
68
66
  // Ignore mailto links even if they use one of the failing patterns. See https://github.com/GoogleChrome/lighthouse/issues/11443#issuecomment-694898412
@@ -86,7 +84,8 @@ class CrawlableAnchors extends Audit {
86
84
  !attributeNames.includes('href') &&
87
85
  hrefAssociatedAttributes.every(attribute => !attributeNames.includes(attribute))
88
86
  ) {
89
- return hasListener;
87
+ // If it has an even listener (e.g. onclick) then we can't assume it's a placeholder. Therefore we consider it failing.
88
+ return Boolean(listeners.length);
90
89
  }
91
90
 
92
91
  if (href === '') return true;
@@ -1,10 +1,5 @@
1
1
  export default ServerResponseTime;
2
2
  declare class ServerResponseTime extends Audit {
3
- /**
4
- * @param {LH.Artifacts.NetworkRequest} record
5
- * @return {number|null}
6
- */
7
- static calculateResponseTime(record: LH.Artifacts.NetworkRequest): number | null;
8
3
  /**
9
4
  * @param {LH.Artifacts} artifacts
10
5
  * @param {LH.Audit.Context} context
@@ -6,7 +6,7 @@
6
6
 
7
7
  import {Audit} from './audit.js';
8
8
  import * as i18n from '../lib/i18n/i18n.js';
9
- import {MainResource} from '../computed/main-resource.js';
9
+ import {NavigationInsights} from '../computed/navigation-insights.js';
10
10
 
11
11
  const UIStrings = {
12
12
  /** Title of a diagnostic audit that provides detail on how long it took from starting a request to when the server started responding. This descriptive title is shown to users when the amount is acceptable and no user action is required. */
@@ -21,9 +21,6 @@ const UIStrings = {
21
21
 
22
22
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
23
23
 
24
- // Due to the way that DevTools throttling works we cannot see if server response took less than ~570ms.
25
- // We set our failure threshold to 600ms to avoid those false positives but we want devs to shoot for 100ms.
26
- const TOO_SLOW_THRESHOLD_MS = 600;
27
24
  const TARGET_MS = 100;
28
25
 
29
26
  class ServerResponseTime extends Audit {
@@ -38,41 +35,30 @@ class ServerResponseTime extends Audit {
38
35
  description: str_(UIStrings.description),
39
36
  supportedModes: ['navigation'],
40
37
  guidanceLevel: 1,
41
- requiredArtifacts: ['DevtoolsLog', 'URL', 'GatherContext'],
38
+ requiredArtifacts: ['Trace', 'SourceMaps'],
42
39
  scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
43
40
  };
44
41
  }
45
42
 
46
- /**
47
- * @param {LH.Artifacts.NetworkRequest} record
48
- * @return {number|null}
49
- */
50
- static calculateResponseTime(record) {
51
- // Lightrider does not have timings for sendEnd, but we do have this timing which should be
52
- // close to the response time.
53
- if (global.isLightrider && record.lrStatistics) return record.lrStatistics.requestMs;
54
-
55
- if (!record.timing) return null;
56
- return record.timing.receiveHeadersStart - record.timing.sendEnd;
57
- }
58
-
59
43
  /**
60
44
  * @param {LH.Artifacts} artifacts
61
45
  * @param {LH.Audit.Context} context
62
46
  * @return {Promise<LH.Audit.Product>}
63
47
  */
64
48
  static async audit(artifacts, context) {
65
- const devtoolsLog = artifacts.DevtoolsLog;
66
-
67
- /** @type {LH.Artifacts.NetworkRequest} */
68
- const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
49
+ const settings = context.settings;
50
+ const trace = artifacts.Trace;
51
+ const SourceMaps = artifacts.SourceMaps;
52
+ const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
53
+ const responseTime = navInsights.model.DocumentLatency.data?.serverResponseTime;
54
+ const url = navInsights.model.DocumentLatency.data?.documentRequest?.args.data.url;
69
55
 
70
- const responseTime = ServerResponseTime.calculateResponseTime(mainResource);
71
- if (responseTime === null) {
56
+ if (responseTime === undefined || !url) {
72
57
  throw new Error('no timing found for main resource');
73
58
  }
74
59
 
75
- const passed = responseTime < TOO_SLOW_THRESHOLD_MS;
60
+ const passed =
61
+ Boolean(navInsights.model.DocumentLatency.data?.checklist.serverResponseIsFast.value);
76
62
  const displayValue = str_(UIStrings.displayValue, {timeInMs: responseTime});
77
63
 
78
64
  /** @type {LH.Audit.Details.Opportunity['headings']} */
@@ -84,7 +70,7 @@ class ServerResponseTime extends Audit {
84
70
  const overallSavingsMs = Math.max(responseTime - TARGET_MS, 0);
85
71
  const details = Audit.makeOpportunityDetails(
86
72
  headings,
87
- [{url: mainResource.url, responseTime}],
73
+ [{url, responseTime}],
88
74
  {overallSavingsMs}
89
75
  );
90
76
 
@@ -2,20 +2,25 @@ export { LCPBreakdownComputed as LCPBreakdown };
2
2
  declare const LCPBreakdownComputed: typeof LCPBreakdown & {
3
3
  request: (dependencies: import("../../index.js").Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext) => Promise<{
4
4
  ttfb: number;
5
- loadStart?: number;
6
- loadEnd?: number;
5
+ loadDelay?: number;
6
+ loadDuration?: number;
7
+ renderDelay?: number;
7
8
  }>;
8
9
  };
10
+ /**
11
+ * Note: this omits renderDelay for simulated throttling.
12
+ */
9
13
  declare class LCPBreakdown {
10
14
  /**
11
15
  * @param {LH.Artifacts.MetricComputationDataInput} data
12
16
  * @param {LH.Artifacts.ComputedContext} context
13
- * @return {Promise<{ttfb: number, loadStart?: number, loadEnd?: number}>}
17
+ * @return {Promise<{ttfb: number, loadDelay?: number, loadDuration?: number, renderDelay?: number}>}
14
18
  */
15
19
  static compute_(data: LH.Artifacts.MetricComputationDataInput, context: LH.Artifacts.ComputedContext): Promise<{
16
20
  ttfb: number;
17
- loadStart?: number;
18
- loadEnd?: number;
21
+ loadDelay?: number;
22
+ loadDuration?: number;
23
+ renderDelay?: number;
19
24
  }>;
20
25
  }
21
26
  //# sourceMappingURL=lcp-breakdown.d.ts.map
@@ -10,42 +10,70 @@ import {LargestContentfulPaint} from './largest-contentful-paint.js';
10
10
  import {ProcessedNavigation} from '../processed-navigation.js';
11
11
  import {TimeToFirstByte} from './time-to-first-byte.js';
12
12
  import {LCPImageRecord} from '../lcp-image-record.js';
13
+ import {NavigationInsights} from '../navigation-insights.js';
13
14
 
15
+ /**
16
+ * Note: this omits renderDelay for simulated throttling.
17
+ */
14
18
  class LCPBreakdown {
15
19
  /**
16
20
  * @param {LH.Artifacts.MetricComputationDataInput} data
17
21
  * @param {LH.Artifacts.ComputedContext} context
18
- * @return {Promise<{ttfb: number, loadStart?: number, loadEnd?: number}>}
22
+ * @return {Promise<{ttfb: number, loadDelay?: number, loadDuration?: number, renderDelay?: number}>}
19
23
  */
20
24
  static async compute_(data, context) {
21
- const processedNavigation = await ProcessedNavigation.request(data.trace, context);
22
- const observedLcp = processedNavigation.timings.largestContentfulPaint;
23
- if (observedLcp === undefined) {
24
- throw new LighthouseError(LighthouseError.errors.NO_LCP);
25
- }
26
- const timeOrigin = processedNavigation.timestamps.timeOrigin / 1000;
25
+ if (data.settings.throttlingMethod === 'simulate') {
26
+ const processedNavigation = await ProcessedNavigation.request(data.trace, context);
27
+ const observedLcp = processedNavigation.timings.largestContentfulPaint;
28
+ if (observedLcp === undefined) {
29
+ throw new LighthouseError(LighthouseError.errors.NO_LCP);
30
+ }
31
+ const timeOrigin = processedNavigation.timestamps.timeOrigin / 1000;
27
32
 
28
- const {timing: ttfb} = await TimeToFirstByte.request(data, context);
33
+ const {timing: ttfb} = await TimeToFirstByte.request(data, context);
29
34
 
30
- const lcpRecord = await LCPImageRecord.request(data, context);
31
- if (!lcpRecord) {
32
- return {ttfb};
33
- }
35
+ const lcpRecord = await LCPImageRecord.request(data, context);
36
+ if (!lcpRecord) {
37
+ return {ttfb};
38
+ }
39
+
40
+ // Official LCP^tm. Will be lantern result if simulated, otherwise same as observedLcp.
41
+ const {timing: metricLcp} = await LargestContentfulPaint.request(data, context);
42
+ const throttleRatio = metricLcp / observedLcp;
34
43
 
35
- // Official LCP^tm. Will be lantern result if simulated, otherwise same as observedLcp.
36
- const {timing: metricLcp} = await LargestContentfulPaint.request(data, context);
37
- const throttleRatio = metricLcp / observedLcp;
44
+ const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
45
+ const loadDelay = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
38
46
 
39
- const unclampedLoadStart = (lcpRecord.networkRequestTime - timeOrigin) * throttleRatio;
40
- const loadStart = Math.max(ttfb, Math.min(unclampedLoadStart, metricLcp));
47
+ const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
48
+ const loadDuration = Math.max(loadDelay, Math.min(unclampedLoadEnd, metricLcp));
41
49
 
42
- const unclampedLoadEnd = (lcpRecord.networkEndTime - timeOrigin) * throttleRatio;
43
- const loadEnd = Math.max(loadStart, Math.min(unclampedLoadEnd, metricLcp));
50
+ return {
51
+ ttfb,
52
+ loadDelay,
53
+ loadDuration,
54
+ };
55
+ }
56
+
57
+ const {trace, settings, SourceMaps} = data;
58
+ const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
59
+ const lcpBreakdown = navInsights.model.LCPBreakdown;
60
+ if (lcpBreakdown instanceof Error) {
61
+ throw new LighthouseError(LighthouseError.errors.NO_LCP, {}, {cause: lcpBreakdown});
62
+ }
63
+
64
+ if (!lcpBreakdown.subparts) {
65
+ throw new LighthouseError(LighthouseError.errors.NO_LCP);
66
+ }
44
67
 
45
68
  return {
46
- ttfb,
47
- loadStart,
48
- loadEnd,
69
+ ttfb: lcpBreakdown.subparts.ttfb.range / 1000,
70
+ loadDelay: lcpBreakdown.subparts.loadDelay !== undefined ?
71
+ lcpBreakdown.subparts.loadDelay.range / 1000 :
72
+ undefined,
73
+ loadDuration: lcpBreakdown.subparts.loadDuration !== undefined ?
74
+ lcpBreakdown.subparts.loadDuration.range / 1000 :
75
+ undefined,
76
+ renderDelay: lcpBreakdown.subparts.renderDelay.range / 1000,
49
77
  };
50
78
  }
51
79
  }
@@ -4,10 +4,14 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import {calculateDocFirstByteTs} from '@paulirish/trace_engine/models/trace/insights/Common.js';
8
+
7
9
  import {makeComputedArtifact} from '../computed-artifact.js';
8
10
  import {NavigationMetric} from './navigation-metric.js';
9
11
  import {MainResource} from '../main-resource.js';
10
12
  import {NetworkAnalysis} from '../network-analysis.js';
13
+ import {NavigationInsights} from '../navigation-insights.js';
14
+ import {TraceEngineResult} from '../trace-engine-result.js';
11
15
 
12
16
  class TimeToFirstByte extends NavigationMetric {
13
17
  /**
@@ -41,18 +45,37 @@ class TimeToFirstByte extends NavigationMetric {
41
45
  * @return {Promise<LH.Artifacts.Metric>}
42
46
  */
43
47
  static async computeObservedMetric(data, context) {
44
- const mainResource = await MainResource.request(data, context);
45
- if (!mainResource.timing) {
46
- throw new Error('missing timing for main resource');
48
+ const {trace, settings, SourceMaps} = data;
49
+ const traceEngineResult =
50
+ await TraceEngineResult.request({trace, settings, SourceMaps}, context);
51
+ const navInsights = await NavigationInsights.request({trace, settings, SourceMaps}, context);
52
+ const lcpBreakdown = navInsights.model.LCPBreakdown;
53
+
54
+ // Defer to LCP breakdown, but if there's no LCP fallback to manual calculation.
55
+ if (!(lcpBreakdown instanceof Error) && lcpBreakdown.subparts) {
56
+ return {
57
+ timing: lcpBreakdown.subparts.ttfb.range / 1000,
58
+ timestamp: lcpBreakdown.subparts.ttfb.max,
59
+ };
60
+ } else if (navInsights.navigation?.args.data?.navigationId) {
61
+ const request = traceEngineResult.data.NetworkRequests.byId.get(
62
+ navInsights.navigation.args.data.navigationId);
63
+ if (!request) {
64
+ throw new Error();
65
+ }
66
+
67
+ const timestamp = calculateDocFirstByteTs(request);
68
+ if (timestamp === null) {
69
+ throw new Error('cannot calculate ttfb');
70
+ }
71
+
72
+ return {
73
+ timing: (timestamp - navInsights.navigation.ts) / 1000,
74
+ timestamp,
75
+ };
47
76
  }
48
77
 
49
- const {processedNavigation} = data;
50
- const timeOriginTs = processedNavigation.timestamps.timeOrigin;
51
- const timestampMs =
52
- mainResource.timing.requestTime * 1000 + mainResource.timing.receiveHeadersStart;
53
- const timestamp = timestampMs * 1000;
54
- const timing = (timestamp - timeOriginTs) / 1000;
55
- return {timing, timestamp};
78
+ throw new Error('cannot determine ttfb');
56
79
  }
57
80
  }
58
81
 
@@ -96,8 +96,9 @@ class TimingSummary {
96
96
  cumulativeLayoutShift,
97
97
  cumulativeLayoutShiftMainFrame,
98
98
 
99
- lcpLoadStart: lcpBreakdown?.loadStart,
100
- lcpLoadEnd: lcpBreakdown?.loadEnd,
99
+ lcpLoadDelay: lcpBreakdown?.loadDelay,
100
+ lcpLoadDuration: lcpBreakdown?.loadDuration,
101
+ lcpRenderDelay: lcpBreakdown?.renderDelay,
101
102
 
102
103
  timeToFirstByte: ttfb?.timing,
103
104
  timeToFirstByteTs: ttfb?.timestamp,
@@ -147,7 +147,6 @@ const defaultConfig = {
147
147
  audits: [
148
148
  'is-on-https',
149
149
  'redirects-http',
150
- 'viewport',
151
150
  'metrics/first-contentful-paint',
152
151
  'metrics/largest-contentful-paint',
153
152
  'metrics/first-meaningful-paint',
@@ -162,7 +161,6 @@ const defaultConfig = {
162
161
  'server-response-time',
163
162
  'metrics/interactive',
164
163
  'user-timings',
165
- 'critical-request-chains',
166
164
  'redirects',
167
165
  'image-aspect-ratio',
168
166
  'image-size-responsive',
@@ -170,8 +168,6 @@ const defaultConfig = {
170
168
  'third-party-cookies',
171
169
  'mainthread-work-breakdown',
172
170
  'bootup-time',
173
- 'uses-rel-preconnect',
174
- 'font-display',
175
171
  'diagnostics',
176
172
  'network-requests',
177
173
  'network-rtt',
@@ -179,15 +175,11 @@ const defaultConfig = {
179
175
  'main-thread-tasks',
180
176
  'metrics',
181
177
  'resource-summary',
182
- 'third-party-summary',
183
- 'largest-contentful-paint-element',
184
- 'lcp-lazy-loaded',
185
178
  'layout-shifts',
186
179
  'long-tasks',
187
180
  'non-composited-animations',
188
181
  'unsized-images',
189
182
  'valid-source-maps',
190
- 'prioritize-lcp-image',
191
183
  'csp-xss',
192
184
  'has-hsts',
193
185
  'origin-isolation',
@@ -267,32 +259,20 @@ const defaultConfig = {
267
259
  'accessibility/manual/offscreen-content-hidden',
268
260
  'accessibility/manual/use-landmarks',
269
261
  'accessibility/manual/visual-order-follows-dom',
270
- 'byte-efficiency/uses-long-cache-ttl',
271
262
  'byte-efficiency/total-byte-weight',
272
263
  'byte-efficiency/offscreen-images',
273
- 'byte-efficiency/render-blocking-resources',
274
264
  'byte-efficiency/unminified-css',
275
265
  'byte-efficiency/unminified-javascript',
276
266
  'byte-efficiency/unused-css-rules',
277
267
  'byte-efficiency/unused-javascript',
278
- 'byte-efficiency/modern-image-formats',
279
- 'byte-efficiency/uses-optimized-images',
280
- 'byte-efficiency/uses-text-compression',
281
- 'byte-efficiency/uses-responsive-images',
282
- 'byte-efficiency/efficient-animated-content',
283
- 'byte-efficiency/duplicated-javascript',
284
- 'byte-efficiency/legacy-javascript',
285
- 'byte-efficiency/uses-responsive-images-snapshot',
286
268
  'dobetterweb/doctype',
287
269
  'dobetterweb/charset',
288
- 'dobetterweb/dom-size',
289
270
  'dobetterweb/geolocation-on-start',
290
271
  'dobetterweb/inspector-issues',
291
272
  'dobetterweb/no-document-write',
292
273
  'dobetterweb/js-libraries',
293
274
  'dobetterweb/notification-on-start',
294
275
  'dobetterweb/paste-preventing-inputs',
295
- 'dobetterweb/uses-http2',
296
276
  'dobetterweb/uses-passive-event-listeners',
297
277
  'seo/meta-description',
298
278
  'seo/http-status-code',
@@ -303,7 +283,6 @@ const defaultConfig = {
303
283
  'seo/hreflang',
304
284
  'seo/canonical',
305
285
  'seo/manual/structured-data',
306
- 'work-during-interaction',
307
286
  'bf-cache',
308
287
  'insights/cache-insight',
309
288
  'insights/cls-culprits-insight',
@@ -407,67 +386,43 @@ const defaultConfig = {
407
386
  {id: 'interaction-to-next-paint', weight: 0, group: 'metrics', acronym: 'INP'},
408
387
 
409
388
  // Insight audits.
410
- {id: 'cache-insight', weight: 0, group: 'hidden'},
411
- {id: 'cls-culprits-insight', weight: 0, group: 'hidden'},
412
- {id: 'document-latency-insight', weight: 0, group: 'hidden'},
413
- {id: 'dom-size-insight', weight: 0, group: 'hidden'},
414
- {id: 'duplicated-javascript-insight', weight: 0, group: 'hidden'},
415
- {id: 'font-display-insight', weight: 0, group: 'hidden'},
416
- {id: 'forced-reflow-insight', weight: 0, group: 'hidden'},
417
- {id: 'image-delivery-insight', weight: 0, group: 'hidden'},
418
- {id: 'inp-breakdown-insight', weight: 0, group: 'hidden'},
419
- {id: 'lcp-breakdown-insight', weight: 0, group: 'hidden'},
420
- {id: 'lcp-discovery-insight', weight: 0, group: 'hidden'},
421
- {id: 'legacy-javascript-insight', weight: 0, group: 'hidden'},
422
- {id: 'modern-http-insight', weight: 0, group: 'hidden'},
423
- {id: 'network-dependency-tree-insight', weight: 0, group: 'hidden'},
424
- {id: 'render-blocking-insight', weight: 0, group: 'hidden'},
425
- {id: 'third-parties-insight', weight: 0, group: 'hidden'},
426
- {id: 'viewport-insight', weight: 0, group: 'hidden'},
389
+ {id: 'cache-insight', weight: 0, group: 'insights'},
390
+ {id: 'cls-culprits-insight', weight: 0, group: 'insights'},
391
+ {id: 'document-latency-insight', weight: 0, group: 'insights'},
392
+ {id: 'dom-size-insight', weight: 0, group: 'insights'},
393
+ {id: 'duplicated-javascript-insight', weight: 0, group: 'insights'},
394
+ {id: 'font-display-insight', weight: 0, group: 'insights'},
395
+ {id: 'forced-reflow-insight', weight: 0, group: 'insights'},
396
+ {id: 'image-delivery-insight', weight: 0, group: 'insights'},
397
+ {id: 'inp-breakdown-insight', weight: 0, group: 'insights'},
398
+ {id: 'lcp-breakdown-insight', weight: 0, group: 'insights'},
399
+ {id: 'lcp-discovery-insight', weight: 0, group: 'insights'},
400
+ {id: 'legacy-javascript-insight', weight: 0, group: 'insights'},
401
+ {id: 'modern-http-insight', weight: 0, group: 'insights'},
402
+ {id: 'network-dependency-tree-insight', weight: 0, group: 'insights'},
403
+ {id: 'render-blocking-insight', weight: 0, group: 'insights'},
404
+ {id: 'third-parties-insight', weight: 0, group: 'insights'},
405
+ {id: 'viewport-insight', weight: 0, group: 'insights'},
427
406
 
428
407
  // These are our "invisible" metrics. Not displayed, but still in the LHR.
429
408
  {id: 'interactive', weight: 0, group: 'hidden', acronym: 'TTI'},
430
409
  {id: 'max-potential-fid', weight: 0, group: 'hidden'},
431
410
  {id: 'first-meaningful-paint', weight: 0, acronym: 'FMP', group: 'hidden'},
432
411
 
433
- {id: 'render-blocking-resources', weight: 0, group: 'diagnostics'},
434
- {id: 'uses-responsive-images', weight: 0, group: 'diagnostics'},
435
412
  {id: 'offscreen-images', weight: 0, group: 'diagnostics'},
436
413
  {id: 'unminified-css', weight: 0, group: 'diagnostics'},
437
414
  {id: 'unminified-javascript', weight: 0, group: 'diagnostics'},
438
415
  {id: 'unused-css-rules', weight: 0, group: 'diagnostics'},
439
416
  {id: 'unused-javascript', weight: 0, group: 'diagnostics'},
440
- {id: 'uses-optimized-images', weight: 0, group: 'diagnostics'},
441
- {id: 'modern-image-formats', weight: 0, group: 'diagnostics'},
442
- {id: 'uses-text-compression', weight: 0, group: 'diagnostics'},
443
- {id: 'uses-rel-preconnect', weight: 0, group: 'diagnostics'},
444
- {id: 'server-response-time', weight: 0, group: 'diagnostics'},
445
- {id: 'redirects', weight: 0, group: 'diagnostics'},
446
- {id: 'uses-http2', weight: 0, group: 'diagnostics'},
447
- {id: 'efficient-animated-content', weight: 0, group: 'diagnostics'},
448
- {id: 'duplicated-javascript', weight: 0, group: 'diagnostics'},
449
- {id: 'legacy-javascript', weight: 0, group: 'diagnostics'},
450
- {id: 'prioritize-lcp-image', weight: 0, group: 'diagnostics'},
451
417
  {id: 'total-byte-weight', weight: 0, group: 'diagnostics'},
452
- {id: 'uses-long-cache-ttl', weight: 0, group: 'diagnostics'},
453
- {id: 'dom-size', weight: 0, group: 'diagnostics'},
454
- {id: 'critical-request-chains', weight: 0, group: 'diagnostics'},
455
418
  {id: 'user-timings', weight: 0, group: 'diagnostics'},
456
419
  {id: 'bootup-time', weight: 0, group: 'diagnostics'},
457
420
  {id: 'mainthread-work-breakdown', weight: 0, group: 'diagnostics'},
458
- {id: 'font-display', weight: 0, group: 'diagnostics'},
459
- {id: 'third-party-summary', weight: 0, group: 'diagnostics'},
460
- {id: 'largest-contentful-paint-element', weight: 0, group: 'diagnostics'},
461
- {id: 'lcp-lazy-loaded', weight: 0, group: 'diagnostics'},
462
- {id: 'layout-shifts', weight: 0, group: 'diagnostics'},
463
421
  {id: 'uses-passive-event-listeners', weight: 0, group: 'diagnostics'},
464
422
  {id: 'no-document-write', weight: 0, group: 'diagnostics'},
465
423
  {id: 'long-tasks', weight: 0, group: 'diagnostics'},
466
424
  {id: 'non-composited-animations', weight: 0, group: 'diagnostics'},
467
425
  {id: 'unsized-images', weight: 0, group: 'diagnostics'},
468
- {id: 'viewport', weight: 0, group: 'diagnostics'},
469
- {id: 'uses-responsive-images-snapshot', weight: 0, group: 'diagnostics'},
470
- {id: 'work-during-interaction', weight: 0, group: 'diagnostics'},
471
426
  {id: 'bf-cache', weight: 0, group: 'diagnostics'},
472
427
 
473
428
  // Audits past this point contain useful data but are not displayed with other audits.
@@ -481,6 +436,9 @@ const defaultConfig = {
481
436
  {id: 'final-screenshot', weight: 0, group: 'hidden'},
482
437
  {id: 'script-treemap-data', weight: 0, group: 'hidden'},
483
438
  {id: 'resource-summary', weight: 0, group: 'hidden'},
439
+ {id: 'redirects', weight: 0, group: 'hidden'},
440
+ {id: 'server-response-time', weight: 0, group: 'hidden'},
441
+ {id: 'layout-shifts', weight: 0, group: 'hidden'},
484
442
  ],
485
443
  },
486
444
  'accessibility': {
@@ -610,7 +568,6 @@ const defaultConfig = {
610
568
  {id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'},
611
569
  {id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'},
612
570
  {id: 'image-size-responsive', weight: 1, group: 'best-practices-ux'},
613
- {id: 'viewport', weight: 1, group: 'best-practices-ux'},
614
571
  // Browser Compatibility
615
572
  {id: 'doctype', weight: 1, group: 'best-practices-browser-compat'},
616
573
  {id: 'charset', weight: 1, group: 'best-practices-browser-compat'},