lighthouse 10.4.0-dev.20230718 → 10.4.0-dev.20230720

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.
@@ -10,10 +10,12 @@ import log from 'lighthouse-logger';
10
10
 
11
11
  const MAXIMUM_WAIT_TIME = 20 * 1000;
12
12
 
13
- // eslint-disable-next-line max-len
13
+ /* eslint-disable max-len */
14
14
  const MESSAGE = `${log.reset}We're constantly trying to improve Lighthouse and its reliability.\n ` +
15
15
  `${log.reset}Learn more: https://github.com/GoogleChrome/lighthouse/blob/main/docs/error-reporting.md \n ` +
16
- ` ${log.bold}May we anonymously report runtime exceptions to improve the tool over time?${log.reset} `; // eslint-disable-line max-len
16
+ ` ${log.bold}May we anonymously report runtime exceptions to improve the tool over time?${log.reset}\n ` +
17
+ `We'll remember your choice, but you can also use the flag --[no-]enable-error-reporting`;
18
+ /* eslint-enable max-len */
17
19
 
18
20
  /**
19
21
  * @return {Promise<boolean>}
@@ -17,7 +17,7 @@ import {Worker, isMainThread, parentPort, workerData} from 'worker_threads';
17
17
  import {once} from 'events';
18
18
 
19
19
  import puppeteer from 'puppeteer-core';
20
- import ChromeLauncher from 'chrome-launcher';
20
+ import * as ChromeLauncher from 'chrome-launcher';
21
21
 
22
22
  import {LH_ROOT} from '../../../../root.js';
23
23
  import {loadArtifacts, saveArtifacts} from '../../../../core/lib/asset-saver.js';
@@ -1,4 +1,3 @@
1
- /// <reference path="../../../../../../types/internal/lighthouse-logger.d.ts" />
2
1
  /**
3
2
  * Launch Chrome and do a full Lighthouse run via the Lighthouse CLI.
4
3
  * @param {string} url
@@ -1,4 +1,3 @@
1
- /// <reference path="../../../../../types/internal/lighthouse-logger.d.ts" />
2
1
  export type Difference = {
3
2
  path: string;
4
3
  actual: any;
@@ -14,6 +14,7 @@ import {LoadSimulator} from '../computed/load-simulator.js';
14
14
  import {ProcessedNavigation} from '../computed/processed-navigation.js';
15
15
  import {PageDependencyGraph} from '../computed/page-dependency-graph.js';
16
16
  import {LanternLargestContentfulPaint} from '../computed/metrics/lantern-largest-contentful-paint.js';
17
+ import {LanternFirstContentfulPaint} from '../computed/metrics/lantern-first-contentful-paint.js';
17
18
 
18
19
  // Preconnect establishes a "clean" socket. Chrome's socket manager will keep an unused socket
19
20
  // around for 10s. Meaning, the time delta between processing preconnect a request should be <10s,
@@ -125,7 +126,8 @@ class UsesRelPreconnectAudit extends Audit {
125
126
  const devtoolsLog = artifacts.devtoolsLogs[UsesRelPreconnectAudit.DEFAULT_PASS];
126
127
  const settings = context.settings;
127
128
 
128
- let maxWasted = 0;
129
+ let maxWastedLcp = 0;
130
+ let maxWastedFcp = 0;
129
131
  /** @type {Array<LH.IcuMessage>} */
130
132
  const warnings = [];
131
133
 
@@ -144,7 +146,14 @@ class UsesRelPreconnectAudit extends Audit {
144
146
  /** @type {Set<string>} */
145
147
  const lcpGraphURLs = new Set();
146
148
  lcpGraph.traverse(node => {
147
- if (node.type === 'network' ) lcpGraphURLs.add(node.record.url);
149
+ if (node.type === 'network') lcpGraphURLs.add(node.record.url);
150
+ });
151
+
152
+ const fcpGraph =
153
+ await LanternFirstContentfulPaint.getPessimisticGraph(pageGraph, processedNavigation);
154
+ const fcpGraphURLs = new Set();
155
+ fcpGraph.traverse(node => {
156
+ if (node.type === 'network') fcpGraphURLs.add(node.record.url);
148
157
  });
149
158
 
150
159
  /** @type {Map<string, LH.Artifacts.NetworkRequest[]>} */
@@ -216,7 +225,11 @@ class UsesRelPreconnectAudit extends Audit {
216
225
  return;
217
226
  }
218
227
 
219
- maxWasted = Math.max(wastedMs, maxWasted);
228
+ maxWastedLcp = Math.max(wastedMs, maxWastedLcp);
229
+
230
+ if (fcpGraphURLs.has(firstRecordOfOrigin.url)) {
231
+ maxWastedFcp = Math.max(wastedMs, maxWastedLcp);
232
+ }
220
233
  results.push({
221
234
  url: securityOrigin,
222
235
  wastedMs: wastedMs,
@@ -240,6 +253,7 @@ class UsesRelPreconnectAudit extends Audit {
240
253
  score: 1,
241
254
  warnings: preconnectLinks.length >= 3 ?
242
255
  [...warnings, str_(UIStrings.tooManyPreconnectLinksWarning)] : warnings,
256
+ metricSavings: {LCP: maxWastedLcp, FCP: maxWastedFcp},
243
257
  };
244
258
  }
245
259
 
@@ -250,17 +264,18 @@ class UsesRelPreconnectAudit extends Audit {
250
264
  ];
251
265
 
252
266
  const details = Audit.makeOpportunityDetails(headings, results,
253
- {overallSavingsMs: maxWasted, sortedBy: ['wastedMs']});
267
+ {overallSavingsMs: maxWastedLcp, sortedBy: ['wastedMs']});
254
268
 
255
269
  return {
256
- score: ByteEfficiencyAudit.scoreForWastedMs(maxWasted),
257
- numericValue: maxWasted,
270
+ score: ByteEfficiencyAudit.scoreForWastedMs(maxWastedLcp),
271
+ numericValue: maxWastedLcp,
258
272
  numericUnit: 'millisecond',
259
- displayValue: maxWasted ?
260
- str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: maxWasted}) :
273
+ displayValue: maxWastedLcp ?
274
+ str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: maxWastedLcp}) :
261
275
  '',
262
276
  warnings,
263
277
  details,
278
+ metricSavings: {LCP: maxWastedLcp, FCP: maxWastedFcp},
264
279
  };
265
280
  }
266
281
  }
@@ -17,7 +17,7 @@ const throttling = {
17
17
  DEVTOOLS_RTT_ADJUSTMENT_FACTOR,
18
18
  DEVTOOLS_THROUGHPUT_ADJUSTMENT_FACTOR,
19
19
  // These values align with WebPageTest's definition of "Fast 3G"
20
- // But offer similar charateristics to roughly the 75th percentile of 4G connections.
20
+ // But offer similar characteristics to roughly the 75th percentile of 4G connections.
21
21
  mobileSlow4G: {
22
22
  rttMs: 150,
23
23
  throughputKbps: 1.6 * 1024,
@@ -108,7 +108,7 @@ class ExecutionContext {
108
108
  ${ExecutionContext._cachedNativesPreamble};
109
109
  globalThis.__lighthouseExecutionContextUniqueIdentifier =
110
110
  ${uniqueExecutionContextIdentifier};
111
- ${pageFunctions.esbuildFunctionNameStubString}
111
+ ${pageFunctions.esbuildFunctionWrapperString}
112
112
  return new Promise(function (resolve) {
113
113
  return Promise.resolve()
114
114
  .then(_ => ${expression})
@@ -277,13 +277,20 @@ class ExecutionContext {
277
277
  * @return {string}
278
278
  */
279
279
  static serializeDeps(deps) {
280
- deps = [pageFunctions.esbuildFunctionNameStubString, ...deps || []];
280
+ deps = [pageFunctions.esbuildFunctionWrapperString, ...deps || []];
281
281
  return deps.map(dep => {
282
282
  if (typeof dep === 'function') {
283
283
  // esbuild will change the actual function name (ie. function actualName() {})
284
- // always, despite minification settings, but preserve the real name in `actualName.name`
285
- // (see esbuildFunctionNameStubString).
286
- return `const ${dep.name} = ${dep}`;
284
+ // always, and preserve the real name in `actualName.name`.
285
+ // See esbuildFunctionWrapperString.
286
+ const output = dep.toString();
287
+ const runtimeName = pageFunctions.getRuntimeFunctionName(dep);
288
+ if (runtimeName !== dep.name) {
289
+ // In addition to exposing the mangled name, also expose the original as an alias.
290
+ return `${output}; const ${dep.name} = ${runtimeName};`;
291
+ } else {
292
+ return output;
293
+ }
287
294
  } else {
288
295
  return dep;
289
296
  }
@@ -23,6 +23,7 @@ import {ProcessedNavigation} from '../../computed/processed-navigation.js';
23
23
  import {LighthouseError} from '../../lib/lh-error.js';
24
24
  import {Responsiveness} from '../../computed/metrics/responsiveness.js';
25
25
  import {CumulativeLayoutShift} from '../../computed/metrics/cumulative-layout-shift.js';
26
+ import {ExecutionContext} from '../driver/execution-context.js';
26
27
 
27
28
  /** @typedef {{nodeId: number, score?: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[], type?: string}} TraceElementData */
28
29
 
@@ -284,12 +285,15 @@ class TraceElements extends BaseGatherer {
284
285
  try {
285
286
  const objectId = await resolveNodeIdToObjectId(session, backendNodeId);
286
287
  if (!objectId) continue;
288
+
289
+ const deps = ExecutionContext.serializeDeps([
290
+ pageFunctions.getNodeDetails,
291
+ getNodeDetailsData,
292
+ ]);
287
293
  response = await session.sendCommand('Runtime.callFunctionOn', {
288
294
  objectId,
289
295
  functionDeclaration: `function () {
290
- ${pageFunctions.esbuildFunctionNameStubString}
291
- ${getNodeDetailsData.toString()};
292
- ${pageFunctions.getNodeDetails};
296
+ ${deps}
293
297
  return getNodeDetailsData.call(this);
294
298
  }`,
295
299
  returnByValue: true,
@@ -65,39 +65,55 @@ export class NetworkAnalyzer {
65
65
  * single handshake.
66
66
  * This is the most accurate and preferred method of measurement when the data is available.
67
67
  *
68
- * @param {LH.Artifacts.NetworkRequest[]} records
69
- * @return {Map<string, number[]>}
68
+ * @param {RequestInfo} info
69
+ * @return {number[]|number|undefined}
70
70
  */
71
- static _estimateRTTByOriginViaConnectionTiming(records: LH.Artifacts.NetworkRequest[]): Map<string, number[]>;
71
+ static _estimateRTTViaConnectionTiming(info: {
72
+ record: LH.Artifacts.NetworkRequest;
73
+ timing: LH.Crdp.Network.ResourceTiming;
74
+ connectionReused?: boolean | undefined;
75
+ }): number[] | number | undefined;
72
76
  /**
73
77
  * Estimates the observed RTT to each origin based on how long a download took on a fresh connection.
74
78
  * NOTE: this will tend to overestimate the actual RTT quite significantly as the download can be
75
79
  * slow for other reasons as well such as bandwidth constraints.
76
80
  *
77
- * @param {LH.Artifacts.NetworkRequest[]} records
78
- * @return {Map<string, number[]>}
81
+ * @param {RequestInfo} info
82
+ * @return {number|undefined}
79
83
  */
80
- static _estimateRTTByOriginViaDownloadTiming(records: LH.Artifacts.NetworkRequest[]): Map<string, number[]>;
84
+ static _estimateRTTViaDownloadTiming(info: {
85
+ record: LH.Artifacts.NetworkRequest;
86
+ timing: LH.Crdp.Network.ResourceTiming;
87
+ connectionReused?: boolean | undefined;
88
+ }): number | undefined;
81
89
  /**
82
90
  * Estimates the observed RTT to each origin based on how long it took until Chrome could
83
91
  * start sending the actual request when a new connection was required.
84
92
  * NOTE: this will tend to overestimate the actual RTT as the request can be delayed for other
85
93
  * reasons as well such as more SSL handshakes if TLS False Start is not enabled.
86
94
  *
87
- * @param {LH.Artifacts.NetworkRequest[]} records
88
- * @return {Map<string, number[]>}
95
+ * @param {RequestInfo} info
96
+ * @return {number|undefined}
89
97
  */
90
- static _estimateRTTByOriginViaSendStartTiming(records: LH.Artifacts.NetworkRequest[]): Map<string, number[]>;
98
+ static _estimateRTTViaSendStartTiming(info: {
99
+ record: LH.Artifacts.NetworkRequest;
100
+ timing: LH.Crdp.Network.ResourceTiming;
101
+ connectionReused?: boolean | undefined;
102
+ }): number | undefined;
91
103
  /**
92
104
  * Estimates the observed RTT to each origin based on how long it took until Chrome received the
93
105
  * headers of the response (~TTFB).
94
106
  * NOTE: this is the most inaccurate way to estimate the RTT, but in some environments it's all
95
107
  * we have access to :(
96
108
  *
97
- * @param {LH.Artifacts.NetworkRequest[]} records
98
- * @return {Map<string, number[]>}
109
+ * @param {RequestInfo} info
110
+ * @return {number|undefined}
99
111
  */
100
- static _estimateRTTByOriginViaHeadersEndTiming(records: LH.Artifacts.NetworkRequest[]): Map<string, number[]>;
112
+ static _estimateRTTViaHeadersEndTiming(info: {
113
+ record: LH.Artifacts.NetworkRequest;
114
+ timing: LH.Crdp.Network.ResourceTiming;
115
+ connectionReused?: boolean | undefined;
116
+ }): number | undefined;
101
117
  /**
102
118
  * Given the RTT to each origin, estimates the observed server response times.
103
119
  *
@@ -132,33 +132,32 @@ class NetworkAnalyzer {
132
132
  * single handshake.
133
133
  * This is the most accurate and preferred method of measurement when the data is available.
134
134
  *
135
- * @param {LH.Artifacts.NetworkRequest[]} records
136
- * @return {Map<string, number[]>}
135
+ * @param {RequestInfo} info
136
+ * @return {number[]|number|undefined}
137
137
  */
138
- static _estimateRTTByOriginViaConnectionTiming(records) {
139
- return NetworkAnalyzer._estimateValueByOrigin(records, ({timing, connectionReused, record}) => {
140
- if (connectionReused) return;
141
-
142
- // In LR, network records are missing connection timing, but we've smuggled it in via headers.
143
- if (global.isLightrider && record.lrStatistics) {
144
- if (record.protocol.startsWith('h3')) {
145
- return record.lrStatistics.TCPMs;
146
- } else {
147
- return [record.lrStatistics.TCPMs / 2, record.lrStatistics.TCPMs / 2];
148
- }
138
+ static _estimateRTTViaConnectionTiming(info) {
139
+ const {timing, connectionReused, record} = info;
140
+ if (connectionReused) return;
141
+
142
+ // In LR, network records are missing connection timing, but we've smuggled it in via headers.
143
+ if (global.isLightrider && record.lrStatistics) {
144
+ if (record.protocol.startsWith('h3')) {
145
+ return record.lrStatistics.TCPMs;
146
+ } else {
147
+ return [record.lrStatistics.TCPMs / 2, record.lrStatistics.TCPMs / 2];
149
148
  }
149
+ }
150
150
 
151
- const {connectStart, sslStart, sslEnd, connectEnd} = timing;
152
- if (connectEnd >= 0 && connectStart >= 0 && record.protocol.startsWith('h3')) {
153
- // These values are equal to sslStart and sslEnd for h3.
154
- return connectEnd - connectStart;
155
- } else if (sslStart >= 0 && sslEnd >= 0 && sslStart !== connectStart) {
156
- // SSL can also be more than 1 RT but assume False Start was used.
157
- return [connectEnd - sslStart, sslStart - connectStart];
158
- } else if (connectStart >= 0 && connectEnd >= 0) {
159
- return connectEnd - connectStart;
160
- }
161
- });
151
+ const {connectStart, sslStart, sslEnd, connectEnd} = timing;
152
+ if (connectEnd >= 0 && connectStart >= 0 && record.protocol.startsWith('h3')) {
153
+ // These values are equal to sslStart and sslEnd for h3.
154
+ return connectEnd - connectStart;
155
+ } else if (sslStart >= 0 && sslEnd >= 0 && sslStart !== connectStart) {
156
+ // SSL can also be more than 1 RT but assume False Start was used.
157
+ return [connectEnd - sslStart, sslStart - connectStart];
158
+ } else if (connectStart >= 0 && connectEnd >= 0) {
159
+ return connectEnd - connectStart;
160
+ }
162
161
  }
163
162
 
164
163
  /**
@@ -166,26 +165,27 @@ class NetworkAnalyzer {
166
165
  * NOTE: this will tend to overestimate the actual RTT quite significantly as the download can be
167
166
  * slow for other reasons as well such as bandwidth constraints.
168
167
  *
169
- * @param {LH.Artifacts.NetworkRequest[]} records
170
- * @return {Map<string, number[]>}
168
+ * @param {RequestInfo} info
169
+ * @return {number|undefined}
171
170
  */
172
- static _estimateRTTByOriginViaDownloadTiming(records) {
173
- return NetworkAnalyzer._estimateValueByOrigin(records, ({record, timing, connectionReused}) => {
174
- if (connectionReused) return;
175
- // Only look at downloads that went past the initial congestion window
176
- if (record.transferSize <= INITIAL_CWD) return;
177
- if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
171
+ static _estimateRTTViaDownloadTiming(info) {
172
+ const {timing, connectionReused, record} = info;
173
+ if (connectionReused) return;
178
174
 
179
- // Compute the amount of time downloading everything after the first congestion window took
180
- const totalTime = record.networkEndTime - record.networkRequestTime;
181
- const downloadTimeAfterFirstByte = totalTime - timing.receiveHeadersEnd;
182
- const numberOfRoundTrips = Math.log2(record.transferSize / INITIAL_CWD);
175
+ // Only look at downloads that went past the initial congestion window
176
+ if (record.transferSize <= INITIAL_CWD) return;
177
+ if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
183
178
 
184
- // Ignore requests that required a high number of round trips since bandwidth starts to play
185
- // a larger role than latency
186
- if (numberOfRoundTrips > 5) return;
187
- return downloadTimeAfterFirstByte / numberOfRoundTrips;
188
- });
179
+ // Compute the amount of time downloading everything after the first congestion window took
180
+ const totalTime = record.networkEndTime - record.networkRequestTime;
181
+ const downloadTimeAfterFirstByte = totalTime - timing.receiveHeadersEnd;
182
+ const numberOfRoundTrips = Math.log2(record.transferSize / INITIAL_CWD);
183
+
184
+ // Ignore requests that required a high number of round trips since bandwidth starts to play
185
+ // a larger role than latency
186
+ if (numberOfRoundTrips > 5) return;
187
+
188
+ return downloadTimeAfterFirstByte / numberOfRoundTrips;
189
189
  }
190
190
 
191
191
  /**
@@ -194,21 +194,21 @@ class NetworkAnalyzer {
194
194
  * NOTE: this will tend to overestimate the actual RTT as the request can be delayed for other
195
195
  * reasons as well such as more SSL handshakes if TLS False Start is not enabled.
196
196
  *
197
- * @param {LH.Artifacts.NetworkRequest[]} records
198
- * @return {Map<string, number[]>}
197
+ * @param {RequestInfo} info
198
+ * @return {number|undefined}
199
199
  */
200
- static _estimateRTTByOriginViaSendStartTiming(records) {
201
- return NetworkAnalyzer._estimateValueByOrigin(records, ({record, timing, connectionReused}) => {
202
- if (connectionReused) return;
203
- if (!Number.isFinite(timing.sendStart) || timing.sendStart < 0) return;
204
-
205
- // Assume everything before sendStart was just DNS + (SSL)? + TCP handshake
206
- // 1 RT for DNS, 1 RT (maybe) for SSL, 1 RT for TCP
207
- let roundTrips = 1;
208
- if (!record.protocol.startsWith('h3')) roundTrips += 1; // TCP
209
- if (record.parsedURL.scheme === 'https') roundTrips += 1;
210
- return timing.sendStart / roundTrips;
211
- });
200
+ static _estimateRTTViaSendStartTiming(info) {
201
+ const {timing, connectionReused, record} = info;
202
+ if (connectionReused) return;
203
+
204
+ if (!Number.isFinite(timing.sendStart) || timing.sendStart < 0) return;
205
+
206
+ // Assume everything before sendStart was just DNS + (SSL)? + TCP handshake
207
+ // 1 RT for DNS, 1 RT (maybe) for SSL, 1 RT for TCP
208
+ let roundTrips = 1;
209
+ if (!record.protocol.startsWith('h3')) roundTrips += 1; // TCP
210
+ if (record.parsedURL.scheme === 'https') roundTrips += 1;
211
+ return timing.sendStart / roundTrips;
212
212
  }
213
213
 
214
214
  /**
@@ -217,34 +217,33 @@ class NetworkAnalyzer {
217
217
  * NOTE: this is the most inaccurate way to estimate the RTT, but in some environments it's all
218
218
  * we have access to :(
219
219
  *
220
- * @param {LH.Artifacts.NetworkRequest[]} records
221
- * @return {Map<string, number[]>}
220
+ * @param {RequestInfo} info
221
+ * @return {number|undefined}
222
222
  */
223
- static _estimateRTTByOriginViaHeadersEndTiming(records) {
224
- return NetworkAnalyzer._estimateValueByOrigin(records, ({record, timing, connectionReused}) => {
225
- if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
226
- if (!record.resourceType) return;
227
-
228
- const serverResponseTimePercentage =
229
- SERVER_RESPONSE_PERCENTAGE_OF_TTFB[record.resourceType] ||
230
- DEFAULT_SERVER_RESPONSE_PERCENTAGE;
231
- const estimatedServerResponseTime = timing.receiveHeadersEnd * serverResponseTimePercentage;
232
-
233
- // When connection was reused...
234
- // TTFB = 1 RT for request + server response time
235
- let roundTrips = 1;
236
-
237
- // When connection was fresh...
238
- // TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
239
- if (!connectionReused) {
240
- roundTrips += 1; // DNS
241
- if (!record.protocol.startsWith('h3')) roundTrips += 1; // TCP
242
- if (record.parsedURL.scheme === 'https') roundTrips += 1; // SSL
243
- }
223
+ static _estimateRTTViaHeadersEndTiming(info) {
224
+ const {timing, connectionReused, record} = info;
225
+ if (!Number.isFinite(timing.receiveHeadersEnd) || timing.receiveHeadersEnd < 0) return;
226
+ if (!record.resourceType) return;
227
+
228
+ const serverResponseTimePercentage =
229
+ SERVER_RESPONSE_PERCENTAGE_OF_TTFB[record.resourceType] ||
230
+ DEFAULT_SERVER_RESPONSE_PERCENTAGE;
231
+ const estimatedServerResponseTime = timing.receiveHeadersEnd * serverResponseTimePercentage;
232
+
233
+ // When connection was reused...
234
+ // TTFB = 1 RT for request + server response time
235
+ let roundTrips = 1;
236
+
237
+ // When connection was fresh...
238
+ // TTFB = DNS + (SSL)? + TCP handshake + 1 RT for request + server response time
239
+ if (!connectionReused) {
240
+ roundTrips += 1; // DNS
241
+ if (!record.protocol.startsWith('h3')) roundTrips += 1; // TCP
242
+ if (record.parsedURL.scheme === 'https') roundTrips += 1; // SSL
243
+ }
244
244
 
245
- // subtract out our estimated server response time
246
- return Math.max((timing.receiveHeadersEnd - estimatedServerResponseTime) / roundTrips, 3);
247
- });
245
+ // subtract out our estimated server response time
246
+ return Math.max((timing.receiveHeadersEnd - estimatedServerResponseTime) / roundTrips, 3);
248
247
  }
249
248
 
250
249
  /**
@@ -350,32 +349,61 @@ class NetworkAnalyzer {
350
349
  useHeadersEndEstimates = true,
351
350
  } = options || {};
352
351
 
353
- let estimatesByOrigin = NetworkAnalyzer._estimateRTTByOriginViaConnectionTiming(records);
354
- if (!estimatesByOrigin.size || forceCoarseEstimates) {
355
- estimatesByOrigin = new Map();
356
- const estimatesViaDownload = NetworkAnalyzer._estimateRTTByOriginViaDownloadTiming(records);
357
- const estimatesViaSendStart = NetworkAnalyzer._estimateRTTByOriginViaSendStartTiming(records);
358
- const estimatesViaTTFB = NetworkAnalyzer._estimateRTTByOriginViaHeadersEndTiming(records);
352
+ const connectionWasReused = NetworkAnalyzer.estimateIfConnectionWasReused(records);
353
+ const groupedByOrigin = NetworkAnalyzer.groupByOrigin(records);
359
354
 
360
- for (const [origin, estimates] of estimatesViaDownload.entries()) {
361
- if (!useDownloadEstimates) continue;
362
- estimatesByOrigin.set(origin, estimates);
355
+ const estimatesByOrigin = new Map();
356
+ for (const [origin, originRecords] of groupedByOrigin.entries()) {
357
+ /** @type {number[]} */
358
+ const originEstimates = [];
359
+
360
+ /**
361
+ * @param {(e: RequestInfo) => number[]|number|undefined} estimator
362
+ */
363
+ // eslint-disable-next-line no-inner-declarations
364
+ function collectEstimates(estimator, multiplier = 1) {
365
+ for (const record of originRecords) {
366
+ const timing = record.timing;
367
+ if (!timing) continue;
368
+
369
+ const estimates = estimator({
370
+ record,
371
+ timing,
372
+ connectionReused: connectionWasReused.get(record.requestId),
373
+ });
374
+ if (estimates === undefined) continue;
375
+
376
+ if (!Array.isArray(estimates)) {
377
+ originEstimates.push(estimates * multiplier);
378
+ } else {
379
+ originEstimates.push(...estimates.map(e => e * multiplier));
380
+ }
381
+ }
363
382
  }
364
383
 
365
- for (const [origin, estimates] of estimatesViaSendStart.entries()) {
366
- if (!useSendStartEstimates) continue;
367
- const existing = estimatesByOrigin.get(origin) || [];
368
- estimatesByOrigin.set(origin, existing.concat(estimates));
384
+ if (!forceCoarseEstimates) {
385
+ collectEstimates(this._estimateRTTViaConnectionTiming);
369
386
  }
370
387
 
371
- for (const [origin, estimates] of estimatesViaTTFB.entries()) {
372
- if (!useHeadersEndEstimates) continue;
373
- const existing = estimatesByOrigin.get(origin) || [];
374
- estimatesByOrigin.set(origin, existing.concat(estimates));
388
+ // Connection timing can be missing for a few reasons:
389
+ // - Origin was preconnected, which we don't have instrumentation for.
390
+ // - Trace began recording after a connection has already been established (for example, in timespan mode)
391
+ // - Perhaps Chrome established a connection already in the background (service worker? Just guessing here)
392
+ // - Not provided in LR netstack.
393
+ if (!originEstimates.length) {
394
+ if (useDownloadEstimates) {
395
+ collectEstimates(this._estimateRTTViaDownloadTiming, coarseEstimateMultiplier);
396
+ }
397
+ if (useSendStartEstimates) {
398
+ collectEstimates(this._estimateRTTViaSendStartTiming, coarseEstimateMultiplier);
399
+ }
400
+ if (useHeadersEndEstimates) {
401
+ collectEstimates(this._estimateRTTViaHeadersEndTiming, coarseEstimateMultiplier);
402
+ }
375
403
  }
376
404
 
377
- for (const estimates of estimatesByOrigin.values()) {
378
- estimates.forEach((x, i) => (estimates[i] = x * coarseEstimateMultiplier));
405
+ if (originEstimates.length) {
406
+ estimatesByOrigin.set(origin, originEstimates);
379
407
  }
380
408
  }
381
409
 
@@ -11,7 +11,8 @@ export namespace pageFunctions {
11
11
  export { wrapRequestIdleCallback };
12
12
  export { getBoundingClientRect };
13
13
  export { truncate };
14
- export { esbuildFunctionNameStubString };
14
+ export { esbuildFunctionWrapperString };
15
+ export { getRuntimeFunctionName };
15
16
  }
16
17
  /**
17
18
  * `typed-query-selector`'s CSS selector parser.
@@ -50,11 +51,11 @@ declare function wrapRuntimeEvalErrorInBrowser(err?: string | Error | undefined)
50
51
  };
51
52
  /**
52
53
  * @template {string} T
53
- * @param {T} selector Optional simple CSS selector to filter nodes on.
54
+ * @param {T=} selector Optional simple CSS selector to filter nodes on.
54
55
  * Combinators are not supported.
55
56
  * @return {Array<ParseSelector<T>>}
56
57
  */
57
- declare function getElementsInDocument<T extends string>(selector: T): import("typed-query-selector/parser").ParseSelector<T, Element>[];
58
+ declare function getElementsInDocument<T extends string>(selector?: T | undefined): import("typed-query-selector/parser").ParseSelector<T, Element>[];
58
59
  /**
59
60
  * Gets the opening tag text of the given node.
60
61
  * @param {Element|ShadowRoot} element
@@ -158,6 +159,11 @@ declare function truncate(string: string, characterLimit: number): string;
158
159
  declare namespace truncate {
159
160
  function toString(): string;
160
161
  }
161
- declare const esbuildFunctionNameStubString: "var __name=(fn)=>fn;";
162
+ declare const esbuildFunctionWrapperString: string;
163
+ /**
164
+ * @param {Function} fn
165
+ * @return {string}
166
+ */
167
+ declare function getRuntimeFunctionName(fn: Function): string;
162
168
  export {};
163
169
  //# sourceMappingURL=page-functions.d.ts.map
@@ -4,6 +4,8 @@
4
4
  * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
5
  */
6
6
 
7
+ import {createRequire} from 'module';
8
+
7
9
  import {Util} from '../../shared/util.js';
8
10
 
9
11
  /**
@@ -50,7 +52,7 @@ function wrapRuntimeEvalErrorInBrowser(err) {
50
52
 
51
53
  /**
52
54
  * @template {string} T
53
- * @param {T} selector Optional simple CSS selector to filter nodes on.
55
+ * @param {T=} selector Optional simple CSS selector to filter nodes on.
54
56
  * Combinators are not supported.
55
57
  * @return {Array<ParseSelector<T>>}
56
58
  */
@@ -513,24 +515,89 @@ function truncate(string, characterLimit) {
513
515
  return Util.truncate(string, characterLimit);
514
516
  }
515
517
 
518
+ function isBundledEnvironment() {
519
+ // If we're in DevTools or LightRider, we are definitely bundled.
520
+ // TODO: refactor and delete `global.isDevtools`.
521
+ if (global.isDevtools || global.isLightrider) return true;
522
+
523
+ const require = createRequire(import.meta.url);
524
+
525
+ try {
526
+ // Not foolproof, but `lighthouse-logger` is a dependency of lighthouse that should always be resolvable.
527
+ // `require.resolve` will only throw in atypical/bundled environments.
528
+ require.resolve('lighthouse-logger');
529
+ return false;
530
+ } catch (err) {
531
+ return true;
532
+ }
533
+ }
534
+
516
535
  // This is to support bundled lighthouse.
517
536
  // esbuild calls every function with a builtin `__name` (since we set keepNames: true),
518
537
  // whose purpose is to store the real name of the function so that esbuild can rename it to avoid
519
538
  // collisions. Anywhere we inject dynamically generated code at runtime for the browser to process,
520
539
  // we must manually include this function (because esbuild only does so once at the top scope of
521
540
  // the bundle, which is irrelevant for code executed in the browser).
522
- const esbuildFunctionNameStubString = 'var __name=(fn)=>fn;';
541
+ // When minified, esbuild will mangle the name of this wrapper function, so we need to determine what it
542
+ // is at runtime in order to recreate it within the page.
543
+ const esbuildFunctionWrapperString = createEsbuildFunctionWrapper();
523
544
 
524
- /** @type {string} */
525
- const truncateRawString = truncate.toString();
526
- truncate.toString = () => `function truncate(string, characterLimit) {
545
+ function createEsbuildFunctionWrapper() {
546
+ if (!isBundledEnvironment()) {
547
+ return '';
548
+ }
549
+
550
+ const functionAsString = (()=>{
551
+ // eslint-disable-next-line no-unused-vars
552
+ const a = ()=>{};
553
+ }).toString()
554
+ // When not minified, esbuild annotates the call to this function wrapper with PURE.
555
+ // We know further that the name of the wrapper function should be `__name`, but let's not
556
+ // hardcode that. Remove the PURE annotation to simplify the regex.
557
+ .replace('/* @__PURE__ */', '');
558
+ const functionStringMatch = functionAsString.match(/=\s*([\w_]+)\(/);
559
+ if (!functionStringMatch) {
560
+ throw new Error('Could not determine esbuild function wrapper name');
561
+ }
562
+
563
+ /**
564
+ * @param {Function} fn
565
+ * @param {string} value
566
+ */
567
+ const esbuildFunctionWrapper = (fn, value) =>
568
+ Object.defineProperty(fn, 'name', {value, configurable: true});
569
+ const wrapperFnName = functionStringMatch[1];
570
+ return `let ${wrapperFnName}=${esbuildFunctionWrapper}`;
571
+ }
572
+
573
+ /**
574
+ * @param {Function} fn
575
+ * @return {string}
576
+ */
577
+ function getRuntimeFunctionName(fn) {
578
+ const match = fn.toString().match(/function ([\w$]+)/);
579
+ if (!match) throw new Error(`could not find function name for: ${fn}`);
580
+ return match[1];
581
+ }
582
+
583
+ // We setup a number of our page functions to automatically include their dependencies.
584
+ // Because of esbuild bundling, we must refer to the actual (mangled) runtime function name.
585
+ /** @type {Record<string, string>} */
586
+ const names = {
587
+ truncate: getRuntimeFunctionName(truncate),
588
+ getNodeLabel: getRuntimeFunctionName(getNodeLabel),
589
+ getOuterHTMLSnippet: getRuntimeFunctionName(getOuterHTMLSnippet),
590
+ getNodeDetails: getRuntimeFunctionName(getNodeDetails),
591
+ };
592
+
593
+ truncate.toString = () => `function ${names.truncate}(string, characterLimit) {
527
594
  const Util = { ${Util.truncate} };
528
- return (${truncateRawString})(string, characterLimit);
595
+ return Util.truncate(string, characterLimit);
529
596
  }`;
530
597
 
531
598
  /** @type {string} */
532
599
  const getNodeLabelRawString = getNodeLabel.toString();
533
- getNodeLabel.toString = () => `function getNodeLabel(element) {
600
+ getNodeLabel.toString = () => `function ${names.getNodeLabel}(element) {
534
601
  ${truncate};
535
602
  return (${getNodeLabelRawString})(element);
536
603
  }`;
@@ -538,14 +605,14 @@ getNodeLabel.toString = () => `function getNodeLabel(element) {
538
605
  /** @type {string} */
539
606
  const getOuterHTMLSnippetRawString = getOuterHTMLSnippet.toString();
540
607
  // eslint-disable-next-line max-len
541
- getOuterHTMLSnippet.toString = () => `function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit = 500) {
608
+ getOuterHTMLSnippet.toString = () => `function ${names.getOuterHTMLSnippet}(element, ignoreAttrs = [], snippetCharacterLimit = 500) {
542
609
  ${truncate};
543
610
  return (${getOuterHTMLSnippetRawString})(element, ignoreAttrs, snippetCharacterLimit);
544
611
  }`;
545
612
 
546
613
  /** @type {string} */
547
614
  const getNodeDetailsRawString = getNodeDetails.toString();
548
- getNodeDetails.toString = () => `function getNodeDetails(element) {
615
+ getNodeDetails.toString = () => `function ${names.getNodeDetails}(element) {
549
616
  ${truncate};
550
617
  ${getNodePath};
551
618
  ${getNodeSelector};
@@ -568,5 +635,6 @@ export const pageFunctions = {
568
635
  wrapRequestIdleCallback,
569
636
  getBoundingClientRect,
570
637
  truncate,
571
- esbuildFunctionNameStubString,
638
+ esbuildFunctionWrapperString,
639
+ getRuntimeFunctionName,
572
640
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "10.4.0-dev.20230718",
4
+ "version": "10.4.0-dev.20230720",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -181,7 +181,7 @@
181
181
  "dependencies": {
182
182
  "@sentry/node": "^6.17.4",
183
183
  "axe-core": "4.7.2",
184
- "chrome-launcher": "^0.15.2",
184
+ "chrome-launcher": "^1.0.0",
185
185
  "configstore": "^5.0.1",
186
186
  "csp_evaluator": "1.1.1",
187
187
  "devtools-protocol": "0.0.1155343",
@@ -190,7 +190,7 @@
190
190
  "intl-messageformat": "^4.4.0",
191
191
  "jpeg-js": "^0.4.4",
192
192
  "js-library-detector": "^6.6.0",
193
- "lighthouse-logger": "^1.4.1",
193
+ "lighthouse-logger": "^2.0.1",
194
194
  "lighthouse-stack-packs": "1.11.0",
195
195
  "lodash": "^4.17.21",
196
196
  "lookup-closest-locale": "6.2.0",
package/readme.md CHANGED
@@ -337,6 +337,8 @@ This section details services that have integrated Lighthouse data. If you're wo
337
337
 
338
338
  * **[Treo](https://treo.sh)** - Treo is Lighthouse as a Service. It provides regression testing, geographical regions, custom networks, and integrations with GitHub & Slack. Treo is a paid product with plans for solo-developers and teams.
339
339
 
340
+ * **[PageVitals](https://pagevitals.com)** - PageVitals combines Lighthouse, CrUX and real-user monitoring data to monitor the performance of websites. See how your website performs over time and get alerted if it gets too slow. Drill down and find the real cause of any performance issue. PageVitals is a paid product with a free 14-day trial.
341
+
340
342
  * **[Alertdesk](https://www.alertdesk.com/)** - Alertdesk is based on Lighthouse and helps you to keep track of your site’s quality & performance. Run daily quality & performance tests from both Mobile and Desktop and dive into the powerful & intuitive reports. You can also monitor your uptime (every minute - 24/7) & domain health. Alertdesk is a paid product with a free 14-day trial.
341
343
 
342
344
  * **[Screpy](https://screpy.com)** - Screpy is a web analysis tool that can analyze all pages of your websites in one dashboard and monitor them with your team. It's powered by Lighthouse and it also includes some different analysis tools (SERP, W3C, Uptime, etc). Screpy has free and paid plans.
@@ -1,34 +0,0 @@
1
- /**
2
- * @license Copyright 2017 The Lighthouse Authors. All Rights Reserved.
3
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4
- * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
5
- */
6
-
7
- declare module 'lighthouse-logger' {
8
- interface Status {
9
- msg: string;
10
- id: string;
11
- args?: any[];
12
- }
13
- export function setLevel(level: string): void;
14
- export function isVerbose(): boolean;
15
- export function formatProtocol(prefix: string, data: Object, level?: string): void;
16
- export function log(title: string, ...args: any[]): void;
17
- export function warn(title: string, ...args: any[]): void;
18
- export function error(title: string, ...args: any[]): void;
19
- export function verbose(title: string, ...args: any[]): void;
20
- export function time(status: Status, level?: string): void;
21
- export function timeEnd(status: Status, level?: string): void;
22
- export function reset(): string;
23
- export const cross: string;
24
- export const dim: string;
25
- export const tick: string;
26
- export const bold: string;
27
- export const purple: string;
28
- export function redify(message: any): string;
29
- export function greenify(message: any): string;
30
- /** Retrieves and clears all stored time entries */
31
- export function takeTimeEntries(): PerformanceEntry[];
32
- export function getTimeEntries(): PerformanceEntry[];
33
- export const events: import('events').EventEmitter;
34
- }