lighthouse 11.0.0-dev.20230814 → 11.0.0-dev.20230815

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.
@@ -7,9 +7,15 @@ declare class DOMSize extends Audit {
7
7
  /**
8
8
  * @param {LH.Artifacts} artifacts
9
9
  * @param {LH.Audit.Context} context
10
- * @return {LH.Audit.Product}
10
+ * @return {Promise<number|undefined>}
11
11
  */
12
- static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): LH.Audit.Product;
12
+ static computeTbtImpact(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<number | undefined>;
13
+ /**
14
+ * @param {LH.Artifacts} artifacts
15
+ * @param {LH.Audit.Context} context
16
+ * @return {Promise<LH.Audit.Product>}
17
+ */
18
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
13
19
  }
14
20
  export namespace UIStrings {
15
21
  const title: string;
@@ -13,6 +13,7 @@
13
13
 
14
14
  import {Audit} from '../audit.js';
15
15
  import * as i18n from '../../lib/i18n/i18n.js';
16
+ import {TBTImpactTasks} from '../../computed/tbt-impact-tasks.js';
16
17
 
17
18
  const UIStrings = {
18
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. */
@@ -53,7 +54,8 @@ class DOMSize extends Audit {
53
54
  failureTitle: str_(UIStrings.failureTitle),
54
55
  description: str_(UIStrings.description),
55
56
  scoreDisplayMode: Audit.SCORING_MODES.NUMERIC,
56
- requiredArtifacts: ['DOMStats'],
57
+ requiredArtifacts: ['DOMStats', 'URL', 'GatherContext'],
58
+ __internalOptionalArtifacts: ['traces', 'devtoolsLogs'],
57
59
  };
58
60
  }
59
61
 
@@ -69,13 +71,53 @@ class DOMSize extends Audit {
69
71
  };
70
72
  }
71
73
 
74
+ /**
75
+ * @param {LH.Artifacts} artifacts
76
+ * @param {LH.Audit.Context} context
77
+ * @return {Promise<number|undefined>}
78
+ */
79
+ static async computeTbtImpact(artifacts, context) {
80
+ let tbtImpact = 0;
81
+
82
+ // We still want to surface this audit in snapshot mode, but since we don't compute TBT
83
+ // the impact should always be undefined.
84
+ const {GatherContext, devtoolsLogs, traces} = artifacts;
85
+ if (GatherContext.gatherMode !== 'navigation') {
86
+ return undefined;
87
+ }
88
+
89
+ // Since the artifacts are optional, it's still possible for them to be missing in navigation mode.
90
+ // Navigation mode does compute TBT so we should surface a numerical savings of 0.
91
+ if (!devtoolsLogs?.[Audit.DEFAULT_PASS] || !traces?.[Audit.DEFAULT_PASS]) {
92
+ return 0;
93
+ }
94
+
95
+ const metricComputationData = Audit.makeMetricComputationDataInput(artifacts, context);
96
+
97
+ try {
98
+ // The TBT impact of style/layout tasks is correlated to the DOM size.
99
+ // Even in situations where the page forces a style recalc, the DOM size is partially to blame
100
+ // for any time spent blocking the main thread.
101
+ //
102
+ // `tbtImpact` should be exactly 0 for small DOMs since `selfTbtImpact` accounts for the blocking
103
+ // time and not the main thread time.
104
+ const tbtImpactTasks = await TBTImpactTasks.request(metricComputationData, context);
105
+ for (const task of tbtImpactTasks) {
106
+ if (task.group.id !== 'styleLayout') continue;
107
+ tbtImpact += task.selfTbtImpact;
108
+ }
109
+ } catch {}
110
+
111
+ return Math.round(tbtImpact);
112
+ }
113
+
72
114
 
73
115
  /**
74
116
  * @param {LH.Artifacts} artifacts
75
117
  * @param {LH.Audit.Context} context
76
- * @return {LH.Audit.Product}
118
+ * @return {Promise<LH.Audit.Product>}
77
119
  */
78
- static audit(artifacts, context) {
120
+ static async audit(artifacts, context) {
79
121
  const stats = artifacts.DOMStats;
80
122
 
81
123
  const score = Audit.computeLogNormalScore(
@@ -120,12 +162,17 @@ class DOMSize extends Audit {
120
162
  },
121
163
  ];
122
164
 
165
+ const tbtImpact = await this.computeTbtImpact(artifacts, context);
166
+
123
167
  return {
124
168
  score,
125
169
  numericValue: stats.totalBodyElements,
126
170
  numericUnit: 'element',
127
171
  displayValue: str_(UIStrings.displayValue, {itemCount: stats.totalBodyElements}),
128
172
  details: Audit.makeTableDetails(headings, items),
173
+ metricSavings: {
174
+ TBT: tbtImpact,
175
+ },
129
176
  };
130
177
  }
131
178
  }
@@ -8,7 +8,8 @@ import log from 'lighthouse-logger';
8
8
 
9
9
  import {Audit} from './audit.js';
10
10
  import * as i18n from '../lib/i18n/i18n.js';
11
- import {LargestContentfulPaint} from '../computed/metrics/largest-contentful-paint.js';
11
+ import {LargestContentfulPaint as LargestContentfulPaintComputed} from '../computed/metrics/largest-contentful-paint.js';
12
+ import LargestContentfulPaint from './metrics/largest-contentful-paint.js';
12
13
  import {LCPBreakdown} from '../computed/metrics/lcp-breakdown.js';
13
14
  import {Sentry} from '../lib/sentry.js';
14
15
 
@@ -124,14 +125,22 @@ class LargestContentfulPaintElement extends Audit {
124
125
  settings: context.settings, URL: artifacts.URL};
125
126
 
126
127
  const elementTable = this.makeElementTable(artifacts);
127
- if (!elementTable) return {score: null, notApplicable: true};
128
+ if (!elementTable) {
129
+ return {
130
+ score: null,
131
+ notApplicable: true,
132
+ metricSavings: {LCP: 0},
133
+ };
134
+ }
128
135
 
129
136
  const items = [elementTable];
130
137
  let displayValue;
138
+ let metricLcp = 0;
131
139
 
132
140
  try {
133
- const {timing: metricLcp} =
134
- await LargestContentfulPaint.request(metricComputationData, context);
141
+ const lcpResult =
142
+ await LargestContentfulPaintComputed.request(metricComputationData, context);
143
+ metricLcp = lcpResult.timing;
135
144
  displayValue = str_(i18n.UIStrings.ms, {timeInMs: metricLcp});
136
145
 
137
146
  const phaseTable = await this.makePhaseTable(metricLcp, metricComputationData, context);
@@ -146,10 +155,18 @@ class LargestContentfulPaintElement extends Audit {
146
155
 
147
156
  const details = Audit.makeListDetails(items);
148
157
 
158
+ // Conceptually, this doesn't make much sense as "savings" for this audit since there isn't anything to "fix".
159
+ // However, this audit will always be useful when improving LCP and that should be reflected in our impact calculations.
160
+ const idealLcp = LargestContentfulPaint.defaultOptions[context.settings.formFactor].scoring.p10;
161
+ const lcpSavings = Math.max(0, metricLcp - idealLcp);
162
+
149
163
  return {
150
164
  score: 1,
151
165
  displayValue,
152
166
  details,
167
+ metricSavings: {
168
+ LCP: lcpSavings,
169
+ },
153
170
  };
154
171
  }
155
172
  }
@@ -8,9 +8,10 @@ declare class LargestContentfulPaintLazyLoaded extends Audit {
8
8
  static isImageInViewport(image: LH.Artifacts.ImageElement, viewportDimensions: LH.Artifacts.ViewportDimensions): boolean;
9
9
  /**
10
10
  * @param {LH.Artifacts} artifacts
11
- * @return {LH.Audit.Product}
11
+ * @param {LH.Audit.Context} context
12
+ * @return {Promise<LH.Audit.Product>}
12
13
  */
13
- static audit(artifacts: LH.Artifacts): LH.Audit.Product;
14
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
14
15
  }
15
16
  export namespace UIStrings {
16
17
  const title: string;
@@ -6,6 +6,8 @@
6
6
 
7
7
  import {Audit} from './audit.js';
8
8
  import * as i18n from '../lib/i18n/i18n.js';
9
+ import {LCPBreakdown} from '../computed/metrics/lcp-breakdown.js';
10
+ import {LargestContentfulPaint} from '../computed/metrics/largest-contentful-paint.js';
9
11
 
10
12
  const UIStrings = {
11
13
  /** Title of a Lighthouse audit that provides detail on whether the largest above-the-fold image was loaded with sufficient priority. This descriptive title is shown to users when the image was loaded properly. */
@@ -18,6 +20,8 @@ const UIStrings = {
18
20
 
19
21
  const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
20
22
 
23
+ const ESTIMATED_PERCENT_SAVINGS = 0.15;
24
+
21
25
  class LargestContentfulPaintLazyLoaded extends Audit {
22
26
  /**
23
27
  * @return {LH.Audit.Meta}
@@ -29,7 +33,8 @@ class LargestContentfulPaintLazyLoaded extends Audit {
29
33
  failureTitle: str_(UIStrings.failureTitle),
30
34
  description: str_(UIStrings.description),
31
35
  supportedModes: ['navigation'],
32
- requiredArtifacts: ['TraceElements', 'ViewportDimensions', 'ImageElements'],
36
+ requiredArtifacts: ['TraceElements', 'ViewportDimensions', 'ImageElements',
37
+ 'traces', 'devtoolsLogs', 'GatherContext', 'URL'],
33
38
  };
34
39
  }
35
40
 
@@ -46,9 +51,10 @@ class LargestContentfulPaintLazyLoaded extends Audit {
46
51
 
47
52
  /**
48
53
  * @param {LH.Artifacts} artifacts
49
- * @return {LH.Audit.Product}
54
+ * @param {LH.Audit.Context} context
55
+ * @return {Promise<LH.Audit.Product>}
50
56
  */
51
- static audit(artifacts) {
57
+ static async audit(artifacts, context) {
52
58
  const lcpElement = artifacts.TraceElements.find(element => {
53
59
  return element.traceEventType === 'largest-contentful-paint' && element.type === 'image';
54
60
  });
@@ -59,7 +65,11 @@ class LargestContentfulPaintLazyLoaded extends Audit {
59
65
 
60
66
  if (!lcpElementImage ||
61
67
  !this.isImageInViewport(lcpElementImage, artifacts.ViewportDimensions)) {
62
- return {score: null, notApplicable: true};
68
+ return {
69
+ score: null,
70
+ notApplicable: true,
71
+ metricSavings: {LCP: 0},
72
+ };
63
73
  }
64
74
 
65
75
  /** @type {LH.Audit.Details.Table['headings']} */
@@ -73,8 +83,27 @@ class LargestContentfulPaintLazyLoaded extends Audit {
73
83
  },
74
84
  ]);
75
85
 
86
+ const wasLazyLoaded = lcpElementImage.loading === 'lazy';
87
+
88
+ const metricComputationData = Audit.makeMetricComputationDataInput(artifacts, context);
89
+ const {timing: metricLcp} =
90
+ await LargestContentfulPaint.request(metricComputationData, context);
91
+ const lcpBreakdown = await LCPBreakdown.request(metricComputationData, context);
92
+ let lcpSavings = 0;
93
+ if (wasLazyLoaded && lcpBreakdown.loadStart !== undefined) {
94
+ // Estimate the LCP savings using a statistical percentage.
95
+ // https://web.dev/lcp-lazy-loading/#causal-performance
96
+ //
97
+ // LCP savings will be at most the LCP load delay.
98
+ const lcpLoadDelay = lcpBreakdown.loadStart - lcpBreakdown.ttfb;
99
+ lcpSavings = Math.min(metricLcp * ESTIMATED_PERCENT_SAVINGS, lcpLoadDelay);
100
+ }
101
+
76
102
  return {
77
- score: lcpElementImage.loading === 'lazy' ? 0 : 1,
103
+ score: wasLazyLoaded ? 0 : 1,
104
+ metricSavings: {
105
+ LCP: lcpSavings,
106
+ },
78
107
  details,
79
108
  };
80
109
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "11.0.0-dev.20230814",
4
+ "version": "11.0.0-dev.20230815",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -56,7 +56,7 @@
56
56
  "unit-viewer": "yarn mocha --testMatch viewer/**/*-test.js",
57
57
  "unit-flow": "bash flow-report/test/run-flow-report-tests.sh",
58
58
  "unit": "yarn unit-flow && yarn mocha",
59
- "unit:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run unit --forbid-only",
59
+ "unit:ci": "NODE_OPTIONS=--max-old-space-size=8192 npm run unit",
60
60
  "core-unit": "yarn unit-core",
61
61
  "cli-unit": "yarn unit-cli",
62
62
  "viewer-unit": "yarn unit-viewer",