lighthouse 13.5.0-dev.20261020 → 13.5.0-dev.20261021
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.
- package/cli/bin.d.ts +2 -2
- package/cli/bin.js +5 -4
- package/cli/cli-flags.d.ts +16 -1
- package/cli/cli-flags.js +51 -0
- package/cli/run.d.ts +5 -5
- package/cli/run.js +97 -47
- package/core/audits/baseline.d.ts +3 -0
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +2 -2
- package/core/computed/lcp-image-record.d.ts +2 -23
- package/core/computed/lcp-image-record.js +3 -67
- package/core/computed/lcp-network-record.d.ts +24 -0
- package/core/computed/lcp-network-record.js +74 -0
- package/core/computed/metrics/lcp-breakdown.js +2 -2
- package/core/config/config-helpers.d.ts +5 -0
- package/core/config/config-helpers.js +6 -24
- package/core/lib/baseline/web-features-data.json +14 -1
- package/dist/report/bundle.esm.js +2 -2
- package/dist/report/flow.js +13 -13
- package/dist/report/standalone.js +2 -2
- package/flow-report/src/util.d.ts +1 -1
- package/package.json +36 -39
- package/readme.md +11 -17
- package/report/generator/file-namer.d.ts +1 -1
- package/report/generator/file-namer.js +4 -3
- package/report/renderer/performance-category-renderer.js +2 -1
- package/report/renderer/text-encoding.js +2 -2
- package/report/types/augment-dom.d.ts +4 -0
- package/types/externs.d.ts +4 -0
package/cli/bin.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @return {Promise<LH.RunnerResult|void>}
|
|
2
|
+
* @return {Promise<LH.RunnerResult|LH.RunnerResult[]|void>}
|
|
3
3
|
*/
|
|
4
|
-
export function begin(): Promise<LH.RunnerResult | void>;
|
|
4
|
+
export function begin(): Promise<LH.RunnerResult | LH.RunnerResult[] | void>;
|
|
5
5
|
//# sourceMappingURL=bin.d.ts.map
|
package/cli/bin.js
CHANGED
|
@@ -41,7 +41,7 @@ function isDev() {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* @return {Promise<LH.RunnerResult|void>}
|
|
44
|
+
* @return {Promise<LH.RunnerResult|LH.RunnerResult[]|void>}
|
|
45
45
|
*/
|
|
46
46
|
async function begin() {
|
|
47
47
|
const cliFlags = getFlags();
|
|
@@ -61,7 +61,7 @@ async function begin() {
|
|
|
61
61
|
commands.listTraceCategories();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const
|
|
64
|
+
const urlsUnderTest = cliFlags._.length > 1 ? cliFlags._ : cliFlags._[0];
|
|
65
65
|
|
|
66
66
|
/** @type {LH.Config|undefined} */
|
|
67
67
|
let config;
|
|
@@ -89,6 +89,7 @@ async function begin() {
|
|
|
89
89
|
log.setLevel(cliFlags.logLevel);
|
|
90
90
|
|
|
91
91
|
if (
|
|
92
|
+
cliFlags._.length <= 1 &&
|
|
92
93
|
cliFlags.output.length === 1 &&
|
|
93
94
|
cliFlags.output[0] === Printer.OutputMode.json &&
|
|
94
95
|
!cliFlags.outputPath
|
|
@@ -120,7 +121,7 @@ async function begin() {
|
|
|
120
121
|
}
|
|
121
122
|
if (cliFlags.enableErrorReporting) {
|
|
122
123
|
await Sentry.init({
|
|
123
|
-
url:
|
|
124
|
+
url: cliFlags._.join(', '),
|
|
124
125
|
flags: cliFlags,
|
|
125
126
|
config,
|
|
126
127
|
environmentData: {
|
|
@@ -131,7 +132,7 @@ async function begin() {
|
|
|
131
132
|
});
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
return runLighthouse(
|
|
135
|
+
return runLighthouse(urlsUnderTest, cliFlags, config);
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
export {
|
package/cli/cli-flags.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function getYargsParser(manualArgv?: string | undefined): import("yargs")
|
|
|
24
24
|
default: boolean;
|
|
25
25
|
describe: string;
|
|
26
26
|
};
|
|
27
|
-
}>, "port" | "screenEmulation" | "emulatedUserAgent" | "hostname" | "preset" | "save-assets" | "list-all-audits" | "list-locales" | "list-trace-categories" | "debug-navigation" | "additional-trace-categories" | "config-path" | "chrome-flags" | "form-factor" | "max-wait-for-load" | "enable-error-reporting" | "gather-mode" | "audit-mode" | "only-audits" | "only-categories" | "skip-audits" | "disable-full-page-screenshot" | "ignore-status-code"> & import("yargs").InferredOptionTypes<{
|
|
27
|
+
}>, "port" | "screenEmulation" | "emulatedUserAgent" | "hostname" | "preset" | "save-assets" | "list-all-audits" | "list-locales" | "list-trace-categories" | "debug-navigation" | "additional-trace-categories" | "config-path" | "chrome-flags" | "form-factor" | "max-wait-for-load" | "enable-error-reporting" | "gather-mode" | "audit-mode" | "only-audits" | "only-categories" | "skip-audits" | "disable-full-page-screenshot" | "ignore-status-code" | "url-file" | "fail-fast"> & import("yargs").InferredOptionTypes<{
|
|
28
28
|
'save-assets': {
|
|
29
29
|
type: "boolean";
|
|
30
30
|
default: boolean;
|
|
@@ -133,6 +133,15 @@ export function getYargsParser(manualArgv?: string | undefined): import("yargs")
|
|
|
133
133
|
type: "boolean";
|
|
134
134
|
describe: string;
|
|
135
135
|
};
|
|
136
|
+
'url-file': {
|
|
137
|
+
type: "string";
|
|
138
|
+
coerce: typeof coerceUrlFile;
|
|
139
|
+
describe: string;
|
|
140
|
+
};
|
|
141
|
+
'fail-fast': {
|
|
142
|
+
type: "boolean";
|
|
143
|
+
describe: string;
|
|
144
|
+
};
|
|
136
145
|
}>, "output" | "view" | "output-path"> & import("yargs").InferredOptionTypes<{
|
|
137
146
|
output: {
|
|
138
147
|
type: "array";
|
|
@@ -221,6 +230,12 @@ declare function coerceOptionalStringBoolean(value: unknown): boolean | string |
|
|
|
221
230
|
* @return {Array<string>=}
|
|
222
231
|
*/
|
|
223
232
|
declare function splitCommaSeparatedValues(strings?: Array<string> | undefined): Array<string> | undefined;
|
|
233
|
+
/**
|
|
234
|
+
* Reads newline-delimited URLs from a file, ignoring empty lines and `#` comments.
|
|
235
|
+
* @param {unknown=} value
|
|
236
|
+
* @return {Array<string>=}
|
|
237
|
+
*/
|
|
238
|
+
declare function coerceUrlFile(value?: unknown | undefined): Array<string> | undefined;
|
|
224
239
|
/**
|
|
225
240
|
* Coerce output CLI input to `LH.SharedFlagsSettings['output']` or throw if not possible.
|
|
226
241
|
* @param {Array<unknown>} values
|
package/cli/cli-flags.js
CHANGED
|
@@ -203,12 +203,22 @@ function getYargsParser(manualArgv) {
|
|
|
203
203
|
type: 'boolean',
|
|
204
204
|
describe: 'Disables failing on all error status codes, and instead issues a warning.',
|
|
205
205
|
},
|
|
206
|
+
'url-file': {
|
|
207
|
+
type: 'string',
|
|
208
|
+
coerce: coerceUrlFile,
|
|
209
|
+
describe: 'The path to a file containing newline-delimited URLs to audit.',
|
|
210
|
+
},
|
|
211
|
+
'fail-fast': {
|
|
212
|
+
type: 'boolean',
|
|
213
|
+
describe: 'Stop auditing remaining URLs immediately if a run fails.',
|
|
214
|
+
},
|
|
206
215
|
})
|
|
207
216
|
.group([
|
|
208
217
|
'save-assets', 'list-all-audits', 'list-locales', 'list-trace-categories', 'additional-trace-categories',
|
|
209
218
|
'config-path', 'preset', 'chrome-flags', 'port', 'hostname', 'form-factor', 'screenEmulation', 'emulatedUserAgent',
|
|
210
219
|
'max-wait-for-load', 'enable-error-reporting', 'gather-mode', 'audit-mode',
|
|
211
220
|
'only-audits', 'only-categories', 'skip-audits', 'disable-full-page-screenshot', 'ignore-status-code',
|
|
221
|
+
'url-file', 'fail-fast',
|
|
212
222
|
], 'Configuration:')
|
|
213
223
|
|
|
214
224
|
// Output
|
|
@@ -305,6 +315,24 @@ Example: --output-path=./lighthouse-results.html`,
|
|
|
305
315
|
.choices('preset', /** @type {const} */ (['perf', 'experimental', 'desktop']))
|
|
306
316
|
|
|
307
317
|
.check(argv => {
|
|
318
|
+
if (Array.isArray(argv.urlFile)) {
|
|
319
|
+
argv._.push(...argv.urlFile);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (argv._.length > 1) {
|
|
323
|
+
if (argv.gatherMode || argv.auditMode) {
|
|
324
|
+
throw new Error('Cannot use -G/--gather-mode or -A/--audit-mode when auditing multiple URLs');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (
|
|
328
|
+
typeof argv.outputPath === 'string' &&
|
|
329
|
+
argv.outputPath &&
|
|
330
|
+
(!fs.existsSync(argv.outputPath) || !fs.lstatSync(argv.outputPath).isDirectory())
|
|
331
|
+
) {
|
|
332
|
+
throw new Error('--output-path must be an existing directory when auditing multiple URLs');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
308
336
|
// Lighthouse doesn't need a URL if...
|
|
309
337
|
// - We're just listing the available options.
|
|
310
338
|
// - We're just printing the config.
|
|
@@ -418,6 +446,29 @@ function coerceOutputPath(value) {
|
|
|
418
446
|
return value;
|
|
419
447
|
}
|
|
420
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Reads newline-delimited URLs from a file, ignoring empty lines and `#` comments.
|
|
451
|
+
* @param {unknown=} value
|
|
452
|
+
* @return {Array<string>=}
|
|
453
|
+
*/
|
|
454
|
+
function coerceUrlFile(value) {
|
|
455
|
+
if (value === undefined) return;
|
|
456
|
+
|
|
457
|
+
if (
|
|
458
|
+
typeof value !== 'string' ||
|
|
459
|
+
!value ||
|
|
460
|
+
!fs.existsSync(value) ||
|
|
461
|
+
fs.lstatSync(value).isDirectory()
|
|
462
|
+
) {
|
|
463
|
+
throw new Error(`--url-file (${value}) cannot be read`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return fs.readFileSync(value, 'utf-8')
|
|
467
|
+
.split(/\r?\n/)
|
|
468
|
+
.map(line => line.trim())
|
|
469
|
+
.filter(line => line.length > 0 && !line.startsWith('#'));
|
|
470
|
+
}
|
|
471
|
+
|
|
421
472
|
/**
|
|
422
473
|
* Verifies value is a string, then coerces type to LH.Locale for convenience. However, don't
|
|
423
474
|
* allowlist specific locales. Why? So we can support the user who requests 'es-MX' (unsupported)
|
package/cli/run.d.ts
CHANGED
|
@@ -11,14 +11,14 @@ export function parseChromeFlags(flags?: string | Array<string>): Array<string>;
|
|
|
11
11
|
/**
|
|
12
12
|
* @param {LH.RunnerResult} runnerResult
|
|
13
13
|
* @param {LH.CliFlags} flags
|
|
14
|
-
* @return {Promise<
|
|
14
|
+
* @return {Promise<string|undefined>}
|
|
15
15
|
*/
|
|
16
|
-
export function saveResults(runnerResult: LH.RunnerResult, flags: LH.CliFlags): Promise<
|
|
16
|
+
export function saveResults(runnerResult: LH.RunnerResult, flags: LH.CliFlags): Promise<string | undefined>;
|
|
17
17
|
/**
|
|
18
|
-
* @param {string}
|
|
18
|
+
* @param {string|string[]|undefined} urlOrUrls
|
|
19
19
|
* @param {LH.CliFlags} flags
|
|
20
20
|
* @param {LH.Config|undefined} config
|
|
21
|
-
* @return {Promise<LH.RunnerResult|undefined>}
|
|
21
|
+
* @return {Promise<LH.RunnerResult|LH.RunnerResult[]|undefined>}
|
|
22
22
|
*/
|
|
23
|
-
export function runLighthouse(
|
|
23
|
+
export function runLighthouse(urlOrUrls: string | string[] | undefined, flags: LH.CliFlags, config: LH.Config | undefined): Promise<LH.RunnerResult | LH.RunnerResult[] | undefined>;
|
|
24
24
|
//# sourceMappingURL=run.d.ts.map
|
package/cli/run.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
/* eslint-disable no-console */
|
|
8
8
|
|
|
9
|
+
import fs from 'fs';
|
|
9
10
|
import path from 'path';
|
|
10
11
|
import os from 'os';
|
|
11
12
|
|
|
@@ -131,7 +132,7 @@ function printErrorAndExit(err) {
|
|
|
131
132
|
/**
|
|
132
133
|
* @param {LH.RunnerResult} runnerResult
|
|
133
134
|
* @param {LH.CliFlags} flags
|
|
134
|
-
* @return {Promise<
|
|
135
|
+
* @return {Promise<string|undefined>}
|
|
135
136
|
*/
|
|
136
137
|
async function saveResults(runnerResult, flags) {
|
|
137
138
|
const cwd = process.cwd();
|
|
@@ -145,43 +146,55 @@ async function saveResults(runnerResult, flags) {
|
|
|
145
146
|
if (!shouldSaveResults) return;
|
|
146
147
|
const {lhr, artifacts, report} = runnerResult;
|
|
147
148
|
|
|
149
|
+
const isDirectoryOutput = Boolean(
|
|
150
|
+
flags.outputPath &&
|
|
151
|
+
fs.existsSync(flags.outputPath) &&
|
|
152
|
+
fs.lstatSync(flags.outputPath).isDirectory()
|
|
153
|
+
);
|
|
154
|
+
|
|
148
155
|
// Use the output path as the prefix for all generated files.
|
|
149
156
|
// If no output path is set, generate a file prefix using the URL and date.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
let configuredPath;
|
|
158
|
+
if (!flags.outputPath || flags.outputPath === 'stdout') {
|
|
159
|
+
configuredPath = getLhrFilenamePrefix(lhr);
|
|
160
|
+
} else if (isDirectoryOutput) {
|
|
161
|
+
configuredPath = path.join(flags.outputPath, getLhrFilenamePrefix(lhr));
|
|
162
|
+
} else {
|
|
163
|
+
configuredPath = flags.outputPath.replace(/\.\w{2,4}$/, '');
|
|
164
|
+
}
|
|
153
165
|
const resolvedPath = path.resolve(cwd, configuredPath);
|
|
154
166
|
|
|
155
167
|
if (flags.saveAssets) {
|
|
156
168
|
await assetSaver.saveAssets(artifacts, lhr.audits, resolvedPath);
|
|
157
169
|
}
|
|
158
170
|
|
|
171
|
+
/** @type {string|undefined} */
|
|
172
|
+
let htmlOutputPath;
|
|
159
173
|
for (const outputType of flags.output) {
|
|
160
174
|
const extension = outputType;
|
|
161
175
|
const output = report[flags.output.indexOf(outputType)];
|
|
162
176
|
let outputPath = `${resolvedPath}.report.${extension}`;
|
|
163
|
-
// If there was only a single output and the user specified
|
|
164
|
-
if (flags.outputPath && flags.output.length === 1)
|
|
177
|
+
// If there was only a single output and the user specified a file outputPath, force usage of it.
|
|
178
|
+
if (flags.outputPath && !isDirectoryOutput && flags.output.length === 1) {
|
|
179
|
+
outputPath = flags.outputPath;
|
|
180
|
+
}
|
|
165
181
|
await Printer.write(output, outputType, outputPath);
|
|
166
182
|
|
|
167
183
|
if (outputType === Printer.OutputMode[Printer.OutputMode.html]) {
|
|
168
|
-
|
|
169
|
-
open(outputPath, {wait: false});
|
|
170
|
-
} else {
|
|
171
|
-
// eslint-disable-next-line max-len
|
|
172
|
-
log.log('CLI', 'Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser');
|
|
173
|
-
}
|
|
184
|
+
htmlOutputPath = outputPath;
|
|
174
185
|
}
|
|
175
186
|
}
|
|
187
|
+
|
|
188
|
+
return htmlOutputPath;
|
|
176
189
|
}
|
|
177
190
|
|
|
178
191
|
/**
|
|
179
|
-
* @param {string}
|
|
192
|
+
* @param {string|string[]|undefined} urlOrUrls
|
|
180
193
|
* @param {LH.CliFlags} flags
|
|
181
194
|
* @param {LH.Config|undefined} config
|
|
182
|
-
* @return {Promise<LH.RunnerResult|undefined>}
|
|
195
|
+
* @return {Promise<LH.RunnerResult|LH.RunnerResult[]|undefined>}
|
|
183
196
|
*/
|
|
184
|
-
async function runLighthouse(
|
|
197
|
+
async function runLighthouse(urlOrUrls, flags, config) {
|
|
185
198
|
/** @param {any} reason */
|
|
186
199
|
function handleTheUnhandled(reason) {
|
|
187
200
|
process.stderr.write(`Unhandled Rejection. Reason: ${reason}\n`);
|
|
@@ -193,48 +206,85 @@ async function runLighthouse(url, flags, config) {
|
|
|
193
206
|
/** @type {ChromeLauncher.LaunchedChrome|undefined} */
|
|
194
207
|
let launchedChrome;
|
|
195
208
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
209
|
+
const urls = [urlOrUrls].flat();
|
|
210
|
+
if (urls.length === 0) urls.push(undefined);
|
|
211
|
+
const isBatch = urls.length > 1;
|
|
200
212
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
flags.port = launchedChrome.port;
|
|
206
|
-
}
|
|
213
|
+
const shouldGather = flags.gatherMode || flags.gatherMode === flags.auditMode;
|
|
214
|
+
const shouldUseLocalChrome = UrlUtils.isLikeLocalhost(flags.hostname);
|
|
215
|
+
const initialPort = flags.port;
|
|
216
|
+
flags.channel = 'cli';
|
|
207
217
|
|
|
208
|
-
|
|
218
|
+
/** @type {Array<LH.RunnerResult>} */
|
|
219
|
+
const results = [];
|
|
220
|
+
/** @type {Array<string>} */
|
|
221
|
+
const htmlReportPaths = [];
|
|
222
|
+
/** @type {Array<ExitError>} */
|
|
223
|
+
const errors = [];
|
|
209
224
|
|
|
210
|
-
|
|
225
|
+
try {
|
|
226
|
+
for (const url of urls) {
|
|
227
|
+
if (url && flags.auditMode && !flags.gatherMode) {
|
|
228
|
+
log.warn('CLI', 'URL parameter is ignored if -A flag is used without -G flag');
|
|
229
|
+
}
|
|
211
230
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
231
|
+
try {
|
|
232
|
+
if (shouldGather && shouldUseLocalChrome && !launchedChrome) {
|
|
233
|
+
launchedChrome = await getDebuggableChrome({...flags, port: initialPort});
|
|
234
|
+
flags.port = launchedChrome.port;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const runnerResult = await lighthouse(url, flags, config);
|
|
238
|
+
|
|
239
|
+
// If in gatherMode only, there will be no runnerResult.
|
|
240
|
+
if (runnerResult) {
|
|
241
|
+
const htmlPath = await saveResults(runnerResult, flags);
|
|
242
|
+
if (htmlPath) htmlReportPaths.push(htmlPath);
|
|
243
|
+
results.push(runnerResult);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Runtime errors indicate something was *very* wrong with the page result.
|
|
247
|
+
// We don't want the user to have to parse the report to figure it out, so we'll still exit
|
|
248
|
+
// with an error code after we saved the results.
|
|
249
|
+
if (runnerResult?.lhr.runtimeError) {
|
|
250
|
+
const {runtimeError} = runnerResult.lhr;
|
|
251
|
+
throw Object.assign(new Error(runtimeError.message), {
|
|
252
|
+
name: 'LighthouseError',
|
|
253
|
+
friendlyMessage: runtimeError.message,
|
|
254
|
+
code: runtimeError.code,
|
|
255
|
+
stack: undefined,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
errors.push(err);
|
|
260
|
+
await launchedChrome?.kill();
|
|
261
|
+
launchedChrome = undefined;
|
|
262
|
+
if (!isBatch || flags.failFast) break;
|
|
263
|
+
const errMsg = err.friendlyMessage || err.message;
|
|
264
|
+
log.error('CLI', `Runtime error encountered for ${url}: ${errMsg}`);
|
|
265
|
+
}
|
|
215
266
|
}
|
|
216
|
-
|
|
217
|
-
launchedChrome?.kill();
|
|
267
|
+
} finally {
|
|
268
|
+
await launchedChrome?.kill();
|
|
218
269
|
process.removeListener('unhandledRejection', handleTheUnhandled);
|
|
270
|
+
}
|
|
219
271
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
code: runtimeError.code,
|
|
229
|
-
message: runtimeError.message,
|
|
230
|
-
});
|
|
272
|
+
if (htmlReportPaths.length > 0) {
|
|
273
|
+
if (flags.view) {
|
|
274
|
+
for (const htmlPath of htmlReportPaths) {
|
|
275
|
+
open(htmlPath, {wait: false});
|
|
276
|
+
}
|
|
277
|
+
} else {
|
|
278
|
+
// eslint-disable-next-line max-len
|
|
279
|
+
log.log('CLI', 'Protip: Run lighthouse with `--view` to immediately open the HTML report in your browser');
|
|
231
280
|
}
|
|
281
|
+
}
|
|
232
282
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
launchedChrome?.kill();
|
|
236
|
-
return printErrorAndExit(err);
|
|
283
|
+
if (errors.length > 0) {
|
|
284
|
+
return printErrorAndExit(errors[0]);
|
|
237
285
|
}
|
|
286
|
+
|
|
287
|
+
return isBatch ? results : results[0];
|
|
238
288
|
}
|
|
239
289
|
|
|
240
290
|
export {
|
|
@@ -656,6 +656,7 @@ declare class Baseline extends Audit {
|
|
|
656
656
|
"word-break": string;
|
|
657
657
|
"word-spacing": string;
|
|
658
658
|
"writing-mode": string;
|
|
659
|
+
"x-frame-options": string;
|
|
659
660
|
xhr: string;
|
|
660
661
|
"xml-serializer": string;
|
|
661
662
|
xpath: string;
|
|
@@ -668,6 +669,7 @@ declare class Baseline extends Audit {
|
|
|
668
669
|
"align-content-block": string;
|
|
669
670
|
alpha: string;
|
|
670
671
|
"alt-text-generated-content": string;
|
|
672
|
+
"animations-css-animation-accessors": string;
|
|
671
673
|
arianotify: string;
|
|
672
674
|
"async-clipboard": string;
|
|
673
675
|
"async-iterable-streams": string;
|
|
@@ -737,6 +739,7 @@ declare class Baseline extends Audit {
|
|
|
737
739
|
"request-video-frame-callback": string;
|
|
738
740
|
requestclose: string;
|
|
739
741
|
"resizable-buffers": string;
|
|
742
|
+
"revert-rule": string;
|
|
740
743
|
rex: string;
|
|
741
744
|
ric: string;
|
|
742
745
|
"round-mod-rem": string;
|
|
@@ -10,7 +10,7 @@ import {NetworkRecords} from '../../computed/network-records.js';
|
|
|
10
10
|
import {LoadSimulator} from '../../computed/load-simulator.js';
|
|
11
11
|
import {LanternLargestContentfulPaint as LanternLCP} from '../../computed/metrics/lantern-largest-contentful-paint.js';
|
|
12
12
|
import {LanternFirstContentfulPaint as LanternFCP} from '../../computed/metrics/lantern-first-contentful-paint.js';
|
|
13
|
-
import {
|
|
13
|
+
import {LCPNetworkRecord} from '../../computed/lcp-network-record.js';
|
|
14
14
|
|
|
15
15
|
const str_ = i18n.createIcuMessageFn(import.meta.url, {});
|
|
16
16
|
|
|
@@ -190,7 +190,7 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
190
190
|
|
|
191
191
|
// The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
|
|
192
192
|
let lcpRecordSavings = 0;
|
|
193
|
-
const lcpRecord = await
|
|
193
|
+
const lcpRecord = await LCPNetworkRecord.request(metricComputationInput, context);
|
|
194
194
|
if (lcpRecord) {
|
|
195
195
|
const lcpResult = results.find(result => result.url === lcpRecord.url);
|
|
196
196
|
if (lcpResult) {
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
3
|
-
request: (dependencies: {
|
|
4
|
-
trace: LH.Trace;
|
|
5
|
-
devtoolsLog: LH.DevtoolsLog;
|
|
6
|
-
}, context: LH.Artifacts.ComputedContext) => Promise<import("../lib/network-request.js").NetworkRequest | undefined>;
|
|
7
|
-
};
|
|
8
|
-
/**
|
|
9
|
-
* @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
|
|
10
|
-
* This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
|
|
11
|
-
* was a pseudo-element associated with a node containing a smaller background-image.
|
|
12
|
-
*/
|
|
13
|
-
declare class LCPImageRecord {
|
|
14
|
-
/**
|
|
15
|
-
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
|
|
16
|
-
* @param {LH.Artifacts.ComputedContext} context
|
|
17
|
-
* @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
|
|
18
|
-
*/
|
|
19
|
-
static compute_(data: {
|
|
20
|
-
trace: LH.Trace;
|
|
21
|
-
devtoolsLog: LH.DevtoolsLog;
|
|
22
|
-
}, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest | undefined>;
|
|
23
|
-
}
|
|
1
|
+
export { LCPNetworkRecord as LCPImageRecord };
|
|
2
|
+
import { LCPNetworkRecord } from './lcp-network-record.js';
|
|
24
3
|
//# sourceMappingURL=lcp-image-record.d.ts.map
|
|
@@ -4,71 +4,7 @@
|
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {ProcessedNavigation} from './processed-navigation.js';
|
|
10
|
-
import {LighthouseError} from '../lib/lh-error.js';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
|
|
14
|
-
* This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
|
|
15
|
-
* was a pseudo-element associated with a node containing a smaller background-image.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
class LCPImageRecord {
|
|
19
|
-
/**
|
|
20
|
-
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
|
|
21
|
-
* @param {LH.Artifacts.ComputedContext} context
|
|
22
|
-
* @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
|
|
23
|
-
*/
|
|
24
|
-
static async compute_(data, context) {
|
|
25
|
-
const {trace, devtoolsLog} = data;
|
|
26
|
-
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
|
|
27
|
-
const processedNavigation = await ProcessedNavigation.request(trace, context);
|
|
28
|
-
if (processedNavigation.timings.largestContentfulPaint === undefined) {
|
|
29
|
-
throw new LighthouseError(LighthouseError.errors.NO_LCP);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Use main-frame-only LCP to match the metric value.
|
|
33
|
-
const lcpEvent = processedNavigation.largestContentfulPaintEvt;
|
|
34
|
-
if (!lcpEvent) return;
|
|
35
|
-
|
|
36
|
-
const lcpImagePaintEvent = trace.traceEvents.filter(e => {
|
|
37
|
-
return e.name === 'LargestImagePaint::Candidate' &&
|
|
38
|
-
e.args.frame === lcpEvent.args.frame &&
|
|
39
|
-
e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
|
|
40
|
-
e.args.data?.size === lcpEvent.args.data?.size;
|
|
41
|
-
// Get last candidate, in case there was more than one.
|
|
42
|
-
}).sort((a, b) => b.ts - a.ts)[0];
|
|
43
|
-
|
|
44
|
-
const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
|
|
45
|
-
if (!lcpUrl) return;
|
|
46
|
-
|
|
47
|
-
const candidates = networkRecords.filter(record => {
|
|
48
|
-
return record.url === lcpUrl &&
|
|
49
|
-
record.finished &&
|
|
50
|
-
// Same frame as LCP trace event.
|
|
51
|
-
record.frameId === lcpImagePaintEvent.args.frame &&
|
|
52
|
-
record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
|
|
53
|
-
}).map(record => {
|
|
54
|
-
// Follow any redirects to find the real image request.
|
|
55
|
-
while (record.redirectDestination) {
|
|
56
|
-
record = record.redirectDestination;
|
|
57
|
-
}
|
|
58
|
-
return record;
|
|
59
|
-
}).filter(record => {
|
|
60
|
-
// Don't select if also loaded by some other means (xhr, etc). `resourceType`
|
|
61
|
-
// isn't set on redirect _sources_, so have to check after following redirects.
|
|
62
|
-
return record.resourceType === 'Image';
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// If there are still multiple candidates, at this point it appears the page
|
|
66
|
-
// simply made multiple requests for the image. The first loaded is the best
|
|
67
|
-
// guess of the request that made the image available for use.
|
|
68
|
-
return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const LCPImageRecordComputed = makeComputedArtifact(LCPImageRecord, ['devtoolsLog', 'trace']);
|
|
73
|
-
export {LCPImageRecordComputed as LCPImageRecord};
|
|
7
|
+
// TODO(v14): remove
|
|
8
|
+
import {LCPNetworkRecord} from './lcp-network-record.js';
|
|
74
9
|
|
|
10
|
+
export {LCPNetworkRecord as LCPImageRecord};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { LCPNetworkRecordComputed as LCPNetworkRecord };
|
|
2
|
+
declare const LCPNetworkRecordComputed: typeof LCPNetworkRecord & {
|
|
3
|
+
request: (dependencies: {
|
|
4
|
+
trace: LH.Trace;
|
|
5
|
+
devtoolsLog: LH.DevtoolsLog;
|
|
6
|
+
}, context: LH.Artifacts.ComputedContext) => Promise<import("../lib/network-request.js").NetworkRequest | undefined>;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
|
|
10
|
+
* This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
|
|
11
|
+
* was a pseudo-element associated with a node containing a smaller background-image.
|
|
12
|
+
*/
|
|
13
|
+
declare class LCPNetworkRecord {
|
|
14
|
+
/**
|
|
15
|
+
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
|
|
16
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
17
|
+
* @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
|
|
18
|
+
*/
|
|
19
|
+
static compute_(data: {
|
|
20
|
+
trace: LH.Trace;
|
|
21
|
+
devtoolsLog: LH.DevtoolsLog;
|
|
22
|
+
}, context: LH.Artifacts.ComputedContext): Promise<LH.Artifacts.NetworkRequest | undefined>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=lcp-network-record.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2023 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {makeComputedArtifact} from './computed-artifact.js';
|
|
8
|
+
import {NetworkRecords} from './network-records.js';
|
|
9
|
+
import {ProcessedNavigation} from './processed-navigation.js';
|
|
10
|
+
import {LighthouseError} from '../lib/lh-error.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
|
|
14
|
+
* This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
|
|
15
|
+
* was a pseudo-element associated with a node containing a smaller background-image.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
class LCPNetworkRecord {
|
|
19
|
+
/**
|
|
20
|
+
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
|
|
21
|
+
* @param {LH.Artifacts.ComputedContext} context
|
|
22
|
+
* @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
|
|
23
|
+
*/
|
|
24
|
+
static async compute_(data, context) {
|
|
25
|
+
const {trace, devtoolsLog} = data;
|
|
26
|
+
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
|
|
27
|
+
const processedNavigation = await ProcessedNavigation.request(trace, context);
|
|
28
|
+
if (processedNavigation.timings.largestContentfulPaint === undefined) {
|
|
29
|
+
throw new LighthouseError(LighthouseError.errors.NO_LCP);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Use main-frame-only LCP to match the metric value.
|
|
33
|
+
const lcpEvent = processedNavigation.largestContentfulPaintEvt;
|
|
34
|
+
if (!lcpEvent) return;
|
|
35
|
+
|
|
36
|
+
const lcpImagePaintEvent = trace.traceEvents.filter(e => {
|
|
37
|
+
return e.name === 'LargestImagePaint::Candidate' &&
|
|
38
|
+
e.args.frame === lcpEvent.args.frame &&
|
|
39
|
+
e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
|
|
40
|
+
e.args.data?.size === lcpEvent.args.data?.size;
|
|
41
|
+
// Get last candidate, in case there was more than one.
|
|
42
|
+
}).sort((a, b) => b.ts - a.ts)[0];
|
|
43
|
+
|
|
44
|
+
const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
|
|
45
|
+
if (!lcpUrl) return;
|
|
46
|
+
|
|
47
|
+
const candidates = networkRecords.filter(record => {
|
|
48
|
+
return record.url === lcpUrl &&
|
|
49
|
+
record.finished &&
|
|
50
|
+
// Same frame as LCP trace event.
|
|
51
|
+
record.frameId === lcpImagePaintEvent.args.frame &&
|
|
52
|
+
record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
|
|
53
|
+
}).map(record => {
|
|
54
|
+
// Follow any redirects to find the real image request.
|
|
55
|
+
while (record.redirectDestination) {
|
|
56
|
+
record = record.redirectDestination;
|
|
57
|
+
}
|
|
58
|
+
return record;
|
|
59
|
+
}).filter(record => {
|
|
60
|
+
// Don't select if also loaded by some other means (xhr, etc). `resourceType`
|
|
61
|
+
// isn't set on redirect _sources_, so have to check after following redirects.
|
|
62
|
+
return record.resourceType === 'Image' || record.resourceType === 'Media';
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// If there are still multiple candidates, at this point it appears the page
|
|
66
|
+
// simply made multiple requests for the image. The first loaded is the best
|
|
67
|
+
// guess of the request that made the image available for use.
|
|
68
|
+
return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const LCPNetworkRecordComputed = makeComputedArtifact(LCPNetworkRecord, ['devtoolsLog', 'trace']);
|
|
73
|
+
export {LCPNetworkRecordComputed as LCPNetworkRecord};
|
|
74
|
+
|
|
@@ -9,7 +9,7 @@ import {LighthouseError} from '../../lib/lh-error.js';
|
|
|
9
9
|
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
|
-
import {
|
|
12
|
+
import {LCPNetworkRecord} from '../lcp-network-record.js';
|
|
13
13
|
import {NavigationInsights} from '../navigation-insights.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -32,7 +32,7 @@ class LCPBreakdown {
|
|
|
32
32
|
|
|
33
33
|
const {timing: ttfb} = await TimeToFirstByte.request(data, context);
|
|
34
34
|
|
|
35
|
-
const lcpRecord = await
|
|
35
|
+
const lcpRecord = await LCPNetworkRecord.request(data, context);
|
|
36
36
|
if (!lcpRecord) {
|
|
37
37
|
return {ttfb};
|
|
38
38
|
}
|