lighthouse 9.6.2 → 9.6.3
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.
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const Audit = require('./audit.js');
|
|
9
9
|
const URL = require('../lib/url-shim.js');
|
|
10
10
|
const NetworkRecords = require('../computed/network-records.js');
|
|
11
|
+
const MainResource = require('../computed/main-resource.js');
|
|
11
12
|
|
|
12
13
|
class NetworkRequests extends Audit {
|
|
13
14
|
/**
|
|
@@ -19,7 +20,7 @@ class NetworkRequests extends Audit {
|
|
|
19
20
|
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
|
|
20
21
|
title: 'Network Requests',
|
|
21
22
|
description: 'Lists the network requests that were made during page load.',
|
|
22
|
-
requiredArtifacts: ['devtoolsLogs'],
|
|
23
|
+
requiredArtifacts: ['devtoolsLogs', 'URL', 'GatherContext'],
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -28,75 +29,91 @@ class NetworkRequests extends Audit {
|
|
|
28
29
|
* @param {LH.Audit.Context} context
|
|
29
30
|
* @return {Promise<LH.Audit.Product>}
|
|
30
31
|
*/
|
|
31
|
-
static audit(artifacts, context) {
|
|
32
|
+
static async audit(artifacts, context) {
|
|
32
33
|
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const records = await NetworkRecords.request(devtoolsLog, context);
|
|
35
|
+
const earliestStartTime = records.reduce(
|
|
36
|
+
(min, record) => Math.min(min, record.startTime),
|
|
37
|
+
Infinity
|
|
38
|
+
);
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
// Optional mainFrameId check because the main resource is only available for
|
|
41
|
+
// navigations. TODO: https://github.com/GoogleChrome/lighthouse/issues/14157
|
|
42
|
+
// for the general solution to this.
|
|
43
|
+
/** @type {string|undefined} */
|
|
44
|
+
let mainFrameId;
|
|
45
|
+
if (artifacts.GatherContext.gatherMode === 'navigation') {
|
|
46
|
+
const mainResource = await MainResource.request({devtoolsLog, URL: artifacts.URL}, context);
|
|
47
|
+
mainFrameId = mainResource.frameId;
|
|
48
|
+
}
|
|
42
49
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const requestMs = record.lrStatistics?.requestMs;
|
|
47
|
-
const responseMs = record.lrStatistics?.responseMs;
|
|
50
|
+
/** @param {number} time */
|
|
51
|
+
const timeToMs = time => time < earliestStartTime || !Number.isFinite(time) ?
|
|
52
|
+
undefined : (time - earliestStartTime) * 1000;
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
resourceType: record.resourceType,
|
|
60
|
-
lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
|
|
61
|
-
lrTCPMs: TCPMs, // Only exists on Lightrider runs
|
|
62
|
-
lrRequestMs: requestMs, // Only exists on Lightrider runs
|
|
63
|
-
lrResponseMs: responseMs, // Only exists on Lightrider runs
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
|
|
68
|
-
/** @type {LH.Audit.Details.Table['headings']} */
|
|
69
|
-
const headings = [
|
|
70
|
-
{key: 'url', itemType: 'url', text: 'URL'},
|
|
71
|
-
{key: 'protocol', itemType: 'text', text: 'Protocol'},
|
|
72
|
-
{key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
|
|
73
|
-
{key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
|
|
74
|
-
{
|
|
75
|
-
key: 'transferSize',
|
|
76
|
-
itemType: 'bytes',
|
|
77
|
-
displayUnit: 'kb',
|
|
78
|
-
granularity: 1,
|
|
79
|
-
text: 'Transfer Size',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
key: 'resourceSize',
|
|
83
|
-
itemType: 'bytes',
|
|
84
|
-
displayUnit: 'kb',
|
|
85
|
-
granularity: 1,
|
|
86
|
-
text: 'Resource Size',
|
|
87
|
-
},
|
|
88
|
-
{key: 'statusCode', itemType: 'text', text: 'Status Code'},
|
|
89
|
-
{key: 'mimeType', itemType: 'text', text: 'MIME Type'},
|
|
90
|
-
{key: 'resourceType', itemType: 'text', text: 'Resource Type'},
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
const tableDetails = Audit.makeTableDetails(headings, results);
|
|
54
|
+
const results = records.map(record => {
|
|
55
|
+
const endTimeDeltaMs = record.lrStatistics?.endTimeDeltaMs;
|
|
56
|
+
const TCPMs = record.lrStatistics?.TCPMs;
|
|
57
|
+
const requestMs = record.lrStatistics?.requestMs;
|
|
58
|
+
const responseMs = record.lrStatistics?.responseMs;
|
|
59
|
+
// Default these to undefined so omitted from JSON in the negative case.
|
|
60
|
+
const isLinkPreload = record.isLinkPreload || undefined;
|
|
61
|
+
const experimentalFromMainFrame = mainFrameId ?
|
|
62
|
+
((record.frameId === mainFrameId) || undefined) :
|
|
63
|
+
undefined;
|
|
94
64
|
|
|
95
65
|
return {
|
|
96
|
-
|
|
97
|
-
|
|
66
|
+
url: URL.elideDataURI(record.url),
|
|
67
|
+
protocol: record.protocol,
|
|
68
|
+
startTime: timeToMs(record.startTime),
|
|
69
|
+
endTime: timeToMs(record.endTime),
|
|
70
|
+
finished: record.finished,
|
|
71
|
+
transferSize: record.transferSize,
|
|
72
|
+
resourceSize: record.resourceSize,
|
|
73
|
+
statusCode: record.statusCode,
|
|
74
|
+
mimeType: record.mimeType,
|
|
75
|
+
resourceType: record.resourceType,
|
|
76
|
+
isLinkPreload,
|
|
77
|
+
experimentalFromMainFrame,
|
|
78
|
+
lrEndTimeDeltaMs: endTimeDeltaMs, // Only exists on Lightrider runs
|
|
79
|
+
lrTCPMs: TCPMs, // Only exists on Lightrider runs
|
|
80
|
+
lrRequestMs: requestMs, // Only exists on Lightrider runs
|
|
81
|
+
lrResponseMs: responseMs, // Only exists on Lightrider runs
|
|
98
82
|
};
|
|
99
83
|
});
|
|
84
|
+
|
|
85
|
+
// NOTE(i18n): this audit is only for debug info in the LHR and does not appear in the report.
|
|
86
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
87
|
+
const headings = [
|
|
88
|
+
{key: 'url', itemType: 'url', text: 'URL'},
|
|
89
|
+
{key: 'protocol', itemType: 'text', text: 'Protocol'},
|
|
90
|
+
{key: 'startTime', itemType: 'ms', granularity: 1, text: 'Start Time'},
|
|
91
|
+
{key: 'endTime', itemType: 'ms', granularity: 1, text: 'End Time'},
|
|
92
|
+
{
|
|
93
|
+
key: 'transferSize',
|
|
94
|
+
itemType: 'bytes',
|
|
95
|
+
displayUnit: 'kb',
|
|
96
|
+
granularity: 1,
|
|
97
|
+
text: 'Transfer Size',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
key: 'resourceSize',
|
|
101
|
+
itemType: 'bytes',
|
|
102
|
+
displayUnit: 'kb',
|
|
103
|
+
granularity: 1,
|
|
104
|
+
text: 'Resource Size',
|
|
105
|
+
},
|
|
106
|
+
{key: 'statusCode', itemType: 'text', text: 'Status Code'},
|
|
107
|
+
{key: 'mimeType', itemType: 'text', text: 'MIME Type'},
|
|
108
|
+
{key: 'resourceType', itemType: 'text', text: 'Resource Type'},
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const tableDetails = Audit.makeTableDetails(headings, results);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
score: 1,
|
|
115
|
+
details: tableDetails,
|
|
116
|
+
};
|
|
100
117
|
}
|
|
101
118
|
}
|
|
102
119
|
|
|
@@ -23,6 +23,10 @@ const UIStrings = {
|
|
|
23
23
|
|
|
24
24
|
const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {Array<{url: string, initiatorType: string}>} InitiatorPath
|
|
28
|
+
*/
|
|
29
|
+
|
|
26
30
|
class PreloadLCPImageAudit extends Audit {
|
|
27
31
|
/**
|
|
28
32
|
* @return {LH.Audit.Meta}
|
|
@@ -86,22 +90,33 @@ class PreloadLCPImageAudit extends Audit {
|
|
|
86
90
|
* @param {LH.Gatherer.Simulation.GraphNode} graph
|
|
87
91
|
* @param {LH.Artifacts.TraceElement|undefined} lcpElement
|
|
88
92
|
* @param {Array<LH.Artifacts.ImageElement>} imageElements
|
|
89
|
-
* @return {LH.Gatherer.Simulation.GraphNetworkNode
|
|
93
|
+
* @return {{lcpNodeToPreload?: LH.Gatherer.Simulation.GraphNetworkNode, initiatorPath?: InitiatorPath}}
|
|
90
94
|
*/
|
|
91
95
|
static getLCPNodeToPreload(mainResource, graph, lcpElement, imageElements) {
|
|
92
|
-
if (!lcpElement) return
|
|
96
|
+
if (!lcpElement) return {};
|
|
93
97
|
|
|
94
98
|
const lcpImageElement = imageElements.find(elem => {
|
|
95
99
|
return elem.node.devtoolsNodePath === lcpElement.node.devtoolsNodePath;
|
|
96
100
|
});
|
|
97
101
|
|
|
98
|
-
if (!lcpImageElement) return
|
|
102
|
+
if (!lcpImageElement) return {};
|
|
99
103
|
const lcpUrl = lcpImageElement.src;
|
|
100
104
|
const {lcpNode, path} = PreloadLCPImageAudit.findLCPNode(graph, lcpUrl);
|
|
101
|
-
if (!lcpNode || !path) return
|
|
105
|
+
if (!lcpNode || !path) return {};
|
|
106
|
+
|
|
102
107
|
// eslint-disable-next-line max-len
|
|
103
108
|
const shouldPreload = PreloadLCPImageAudit.shouldPreloadRequest(lcpNode.record, mainResource, path);
|
|
104
|
-
|
|
109
|
+
const lcpNodeToPreload = shouldPreload ? lcpNode : undefined;
|
|
110
|
+
|
|
111
|
+
const initiatorPath = [
|
|
112
|
+
{url: lcpNode.record.url, initiatorType: lcpNode.initiatorType},
|
|
113
|
+
...path.map(n => ({url: n.record.url, initiatorType: n.initiatorType})),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
lcpNodeToPreload,
|
|
118
|
+
initiatorPath,
|
|
119
|
+
};
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
/**
|
|
@@ -215,10 +230,10 @@ class PreloadLCPImageAudit extends Audit {
|
|
|
215
230
|
|
|
216
231
|
const graph = lanternLCP.pessimisticGraph;
|
|
217
232
|
// eslint-disable-next-line max-len
|
|
218
|
-
const
|
|
233
|
+
const {lcpNodeToPreload, initiatorPath} = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
|
|
219
234
|
|
|
220
235
|
const {results, wastedMs} =
|
|
221
|
-
PreloadLCPImageAudit.computeWasteWithGraph(lcpElement,
|
|
236
|
+
PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
|
|
222
237
|
|
|
223
238
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
224
239
|
const headings = [
|
|
@@ -228,6 +243,17 @@ class PreloadLCPImageAudit extends Audit {
|
|
|
228
243
|
];
|
|
229
244
|
const details = Audit.makeOpportunityDetails(headings, results, wastedMs);
|
|
230
245
|
|
|
246
|
+
// If LCP element was an image and had valid network records (regardless of
|
|
247
|
+
// if it should be preloaded), it will be found first in the `initiatorPath`.
|
|
248
|
+
// Otherwise path and length will be undefined.
|
|
249
|
+
if (initiatorPath) {
|
|
250
|
+
details.debugData = {
|
|
251
|
+
type: 'debugdata',
|
|
252
|
+
initiatorPath,
|
|
253
|
+
pathLength: initiatorPath.length,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
231
257
|
return {
|
|
232
258
|
score: UnusedBytes.scoreForWastedMs(wastedMs),
|
|
233
259
|
numericValue: wastedMs,
|