lighthouse 10.4.0-dev.20230801 → 10.4.0-dev.20230802
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/core/audits/dobetterweb/uses-http2.d.ts +18 -1
- package/core/audits/dobetterweb/uses-http2.js +62 -16
- package/core/audits/largest-contentful-paint-element.d.ts +0 -6
- package/core/audits/largest-contentful-paint-element.js +13 -15
- package/core/config/constants.js +1 -1
- package/core/config/default-config.js +3 -3
- package/core/gather/navigation-runner.js +8 -2
- package/core/lib/asset-saver.js +6 -0
- package/core/lib/network-request.js +12 -3
- package/core/runner.js +2 -0
- package/package.json +1 -1
- package/types/artifacts.d.ts +4 -0
|
@@ -2,12 +2,29 @@ export default UsesHTTP2Audit;
|
|
|
2
2
|
export type Simulator = import('../../lib/dependency-graph/simulator/simulator').Simulator;
|
|
3
3
|
export type Node = import('../../lib/dependency-graph/base-node.js').Node;
|
|
4
4
|
declare class UsesHTTP2Audit extends Audit {
|
|
5
|
+
/**
|
|
6
|
+
* Computes the estimated effect of all results being converted to http/2 on the provided graph.
|
|
7
|
+
*
|
|
8
|
+
* @param {Array<{url: string}>} results
|
|
9
|
+
* @param {Node} graph
|
|
10
|
+
* @param {Simulator} simulator
|
|
11
|
+
* @param {{label?: string}=} options
|
|
12
|
+
* @return {{savings: number, simulationBefore: LH.Gatherer.Simulation.Result, simulationAfter: LH.Gatherer.Simulation.Result}}
|
|
13
|
+
*/
|
|
14
|
+
static computeWasteWithGraph(results: Array<{
|
|
15
|
+
url: string;
|
|
16
|
+
}>, graph: Node, simulator: Simulator, options?: {
|
|
17
|
+
label?: string;
|
|
18
|
+
} | undefined): {
|
|
19
|
+
savings: number;
|
|
20
|
+
simulationBefore: LH.Gatherer.Simulation.Result;
|
|
21
|
+
simulationAfter: LH.Gatherer.Simulation.Result;
|
|
22
|
+
};
|
|
5
23
|
/**
|
|
6
24
|
* Computes the estimated effect all results being converted to use http/2, the max of:
|
|
7
25
|
*
|
|
8
26
|
* - end time of the last long task in the provided graph
|
|
9
27
|
* - end time of the last node in the graph
|
|
10
|
-
*
|
|
11
28
|
* @param {Array<{url: string}>} results
|
|
12
29
|
* @param {Node} graph
|
|
13
30
|
* @param {Simulator} simulator
|
|
@@ -21,6 +21,8 @@ import {NetworkRequest} from '../../lib/network-request.js';
|
|
|
21
21
|
import {NetworkRecords} from '../../computed/network-records.js';
|
|
22
22
|
import {LoadSimulator} from '../../computed/load-simulator.js';
|
|
23
23
|
import {PageDependencyGraph} from '../../computed/page-dependency-graph.js';
|
|
24
|
+
import {LanternLargestContentfulPaint} from '../../computed/metrics/lantern-largest-contentful-paint.js';
|
|
25
|
+
import {LanternFirstContentfulPaint} from '../../computed/metrics/lantern-first-contentful-paint.js';
|
|
24
26
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
25
27
|
|
|
26
28
|
const UIStrings = {
|
|
@@ -66,23 +68,23 @@ class UsesHTTP2Audit extends Audit {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
* Computes the estimated effect all results being converted to
|
|
70
|
-
*
|
|
71
|
-
* - end time of the last long task in the provided graph
|
|
72
|
-
* - end time of the last node in the graph
|
|
71
|
+
* Computes the estimated effect of all results being converted to http/2 on the provided graph.
|
|
73
72
|
*
|
|
74
73
|
* @param {Array<{url: string}>} results
|
|
75
74
|
* @param {Node} graph
|
|
76
75
|
* @param {Simulator} simulator
|
|
77
|
-
* @
|
|
76
|
+
* @param {{label?: string}=} options
|
|
77
|
+
* @return {{savings: number, simulationBefore: LH.Gatherer.Simulation.Result, simulationAfter: LH.Gatherer.Simulation.Result}}
|
|
78
78
|
*/
|
|
79
|
-
static
|
|
80
|
-
|
|
81
|
-
const
|
|
79
|
+
static computeWasteWithGraph(results, graph, simulator, options) {
|
|
80
|
+
options = Object.assign({label: ''}, options);
|
|
81
|
+
const beforeLabel = `${this.meta.id}-${options.label}-before`;
|
|
82
|
+
const afterLabel = `${this.meta.id}-${options.label}-after`;
|
|
82
83
|
const flexibleOrdering = true;
|
|
83
84
|
|
|
84
85
|
const urlsToChange = new Set(results.map(result => result.url));
|
|
85
|
-
const simulationBefore =
|
|
86
|
+
const simulationBefore =
|
|
87
|
+
simulator.simulate(graph, {label: beforeLabel, flexibleOrdering});
|
|
86
88
|
|
|
87
89
|
// Update all the protocols to reflect implementing our recommendations
|
|
88
90
|
/** @type {Map<string, string>} */
|
|
@@ -105,9 +107,36 @@ class UsesHTTP2Audit extends Audit {
|
|
|
105
107
|
node.record.protocol = originalProtocol;
|
|
106
108
|
});
|
|
107
109
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
+
const savings = simulationBefore.timeInMs - simulationAfter.timeInMs;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
// Round waste to nearest 10ms
|
|
114
|
+
savings: Math.round(Math.max(savings, 0) / 10) * 10,
|
|
115
|
+
simulationBefore,
|
|
116
|
+
simulationAfter,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Computes the estimated effect all results being converted to use http/2, the max of:
|
|
122
|
+
*
|
|
123
|
+
* - end time of the last long task in the provided graph
|
|
124
|
+
* - end time of the last node in the graph
|
|
125
|
+
* @param {Array<{url: string}>} results
|
|
126
|
+
* @param {Node} graph
|
|
127
|
+
* @param {Simulator} simulator
|
|
128
|
+
* @return {number}
|
|
129
|
+
*/
|
|
130
|
+
static computeWasteWithTTIGraph(results, graph, simulator) {
|
|
131
|
+
const {savings: savingsOnOverallLoad, simulationBefore, simulationAfter} =
|
|
132
|
+
this.computeWasteWithGraph(results, graph, simulator, {
|
|
133
|
+
label: 'tti',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const savingsOnTTI =
|
|
137
|
+
LanternInteractive.getLastLongTaskEndTime(simulationBefore.nodeTimings) -
|
|
110
138
|
LanternInteractive.getLastLongTaskEndTime(simulationAfter.nodeTimings);
|
|
139
|
+
|
|
111
140
|
const savings = Math.max(savingsOnTTI, savingsOnOverallLoad);
|
|
112
141
|
|
|
113
142
|
// Round waste to nearest 10ms
|
|
@@ -237,9 +266,25 @@ class UsesHTTP2Audit extends Audit {
|
|
|
237
266
|
devtoolsLog,
|
|
238
267
|
settings,
|
|
239
268
|
};
|
|
240
|
-
const
|
|
269
|
+
const pageGraph = await PageDependencyGraph.request({trace, devtoolsLog, URL}, context);
|
|
241
270
|
const simulator = await LoadSimulator.request(simulatorOptions, context);
|
|
242
|
-
const
|
|
271
|
+
const metricComputationInput = Audit.makeMetricComputationDataInput(artifacts, context);
|
|
272
|
+
|
|
273
|
+
const {
|
|
274
|
+
pessimisticGraph: fcpGraph,
|
|
275
|
+
} = await LanternFirstContentfulPaint.request(metricComputationInput, context);
|
|
276
|
+
const {
|
|
277
|
+
pessimisticGraph: lcpGraph,
|
|
278
|
+
} = await LanternLargestContentfulPaint.request(metricComputationInput, context);
|
|
279
|
+
|
|
280
|
+
const wastedMsTti = UsesHTTP2Audit.computeWasteWithTTIGraph(
|
|
281
|
+
resources, pageGraph, simulator);
|
|
282
|
+
const wasteFcp =
|
|
283
|
+
UsesHTTP2Audit.computeWasteWithGraph(resources,
|
|
284
|
+
fcpGraph, simulator, {label: 'fcp'});
|
|
285
|
+
const wasteLcp =
|
|
286
|
+
UsesHTTP2Audit.computeWasteWithGraph(resources,
|
|
287
|
+
lcpGraph, simulator, {label: 'lcp'});
|
|
243
288
|
|
|
244
289
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
245
290
|
const headings = [
|
|
@@ -248,14 +293,15 @@ class UsesHTTP2Audit extends Audit {
|
|
|
248
293
|
];
|
|
249
294
|
|
|
250
295
|
const details = Audit.makeOpportunityDetails(headings, resources,
|
|
251
|
-
{overallSavingsMs:
|
|
296
|
+
{overallSavingsMs: wastedMsTti});
|
|
252
297
|
|
|
253
298
|
return {
|
|
254
299
|
displayValue,
|
|
255
|
-
numericValue:
|
|
300
|
+
numericValue: wastedMsTti,
|
|
256
301
|
numericUnit: 'millisecond',
|
|
257
|
-
score: ByteEfficiencyAudit.scoreForWastedMs(
|
|
302
|
+
score: ByteEfficiencyAudit.scoreForWastedMs(wastedMsTti),
|
|
258
303
|
details,
|
|
304
|
+
metricSavings: {LCP: wasteLcp.savings, FCP: wasteFcp.savings},
|
|
259
305
|
};
|
|
260
306
|
}
|
|
261
307
|
}
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
export default LargestContentfulPaintElement;
|
|
2
2
|
declare class LargestContentfulPaintElement extends Audit {
|
|
3
|
-
/**
|
|
4
|
-
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
5
|
-
* @param {LH.Audit.Context} context
|
|
6
|
-
* @return {Promise<number|undefined>}
|
|
7
|
-
*/
|
|
8
|
-
static getOptionalLCPMetric(metricComputationData: LH.Artifacts.MetricComputationDataInput, context: LH.Audit.Context): Promise<number | undefined>;
|
|
9
3
|
/**
|
|
10
4
|
* @param {LH.Artifacts} artifacts
|
|
11
5
|
* @return {LH.Audit.Details.Table|undefined}
|
|
@@ -4,10 +4,13 @@
|
|
|
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 log from 'lighthouse-logger';
|
|
8
|
+
|
|
7
9
|
import {Audit} from './audit.js';
|
|
8
10
|
import * as i18n from '../lib/i18n/i18n.js';
|
|
9
11
|
import {LargestContentfulPaint} from '../computed/metrics/largest-contentful-paint.js';
|
|
10
12
|
import {LCPBreakdown} from '../computed/metrics/lcp-breakdown.js';
|
|
13
|
+
import {Sentry} from '../lib/sentry.js';
|
|
11
14
|
|
|
12
15
|
const UIStrings = {
|
|
13
16
|
/** Descriptive title of a diagnostic audit that provides the element that was determined to be the Largest Contentful Paint. */
|
|
@@ -49,19 +52,6 @@ class LargestContentfulPaintElement extends Audit {
|
|
|
49
52
|
};
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
/**
|
|
53
|
-
* @param {LH.Artifacts.MetricComputationDataInput} metricComputationData
|
|
54
|
-
* @param {LH.Audit.Context} context
|
|
55
|
-
* @return {Promise<number|undefined>}
|
|
56
|
-
*/
|
|
57
|
-
static async getOptionalLCPMetric(metricComputationData, context) {
|
|
58
|
-
try {
|
|
59
|
-
const {timing: metricLcp} =
|
|
60
|
-
await LargestContentfulPaint.request(metricComputationData, context);
|
|
61
|
-
return metricLcp;
|
|
62
|
-
} catch {}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
55
|
/**
|
|
66
56
|
* @param {LH.Artifacts} artifacts
|
|
67
57
|
* @return {LH.Audit.Details.Table|undefined}
|
|
@@ -139,11 +129,19 @@ class LargestContentfulPaintElement extends Audit {
|
|
|
139
129
|
const items = [elementTable];
|
|
140
130
|
let displayValue;
|
|
141
131
|
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
try {
|
|
133
|
+
const {timing: metricLcp} =
|
|
134
|
+
await LargestContentfulPaint.request(metricComputationData, context);
|
|
144
135
|
displayValue = str_(i18n.UIStrings.ms, {timeInMs: metricLcp});
|
|
136
|
+
|
|
145
137
|
const phaseTable = await this.makePhaseTable(metricLcp, metricComputationData, context);
|
|
146
138
|
items.push(phaseTable);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
Sentry.captureException(err, {
|
|
141
|
+
tags: {audit: this.meta.id},
|
|
142
|
+
level: 'error',
|
|
143
|
+
});
|
|
144
|
+
log.error(this.meta.id, err.message);
|
|
147
145
|
}
|
|
148
146
|
|
|
149
147
|
const details = Audit.makeListDetails(items);
|
package/core/config/constants.js
CHANGED
|
@@ -129,7 +129,7 @@ const defaultSettings = {
|
|
|
129
129
|
|
|
130
130
|
/** @type {Required<LH.Config.NavigationJson>} */
|
|
131
131
|
const defaultNavigationConfig = {
|
|
132
|
-
id: '
|
|
132
|
+
id: 'defaultPass',
|
|
133
133
|
loadFailureMode: 'fatal',
|
|
134
134
|
disableThrottling: false,
|
|
135
135
|
disableStorageReset: false,
|
|
@@ -593,12 +593,12 @@ const defaultConfig = {
|
|
|
593
593
|
supportedModes: ['navigation', 'timespan', 'snapshot'],
|
|
594
594
|
auditRefs: [
|
|
595
595
|
// Trust & Safety
|
|
596
|
-
{id: 'is-on-https', weight:
|
|
596
|
+
{id: 'is-on-https', weight: 5, group: 'best-practices-trust-safety'},
|
|
597
597
|
{id: 'geolocation-on-start', weight: 1, group: 'best-practices-trust-safety'},
|
|
598
598
|
{id: 'notification-on-start', weight: 1, group: 'best-practices-trust-safety'},
|
|
599
599
|
{id: 'csp-xss', weight: 0, group: 'best-practices-trust-safety'},
|
|
600
600
|
// User Experience
|
|
601
|
-
{id: 'paste-preventing-inputs', weight:
|
|
601
|
+
{id: 'paste-preventing-inputs', weight: 3, group: 'best-practices-ux'},
|
|
602
602
|
{id: 'image-aspect-ratio', weight: 1, group: 'best-practices-ux'},
|
|
603
603
|
{id: 'image-size-responsive', weight: 1, group: 'best-practices-ux'},
|
|
604
604
|
{id: 'preload-fonts', weight: 1, group: 'best-practices-ux'},
|
|
@@ -608,7 +608,7 @@ const defaultConfig = {
|
|
|
608
608
|
// General Group
|
|
609
609
|
{id: 'no-unload-listeners', weight: 1, group: 'best-practices-general'},
|
|
610
610
|
{id: 'js-libraries', weight: 0, group: 'best-practices-general'},
|
|
611
|
-
{id: 'deprecations', weight:
|
|
611
|
+
{id: 'deprecations', weight: 5, group: 'best-practices-general'},
|
|
612
612
|
{id: 'errors-in-console', weight: 1, group: 'best-practices-general'},
|
|
613
613
|
{id: 'valid-source-maps', weight: 0, group: 'best-practices-general'},
|
|
614
614
|
{id: 'inspector-issues', weight: 1, group: 'best-practices-general'},
|
|
@@ -188,8 +188,14 @@ async function _computeNavigationResult(
|
|
|
188
188
|
/** @type {Partial<LH.GathererArtifacts>} */
|
|
189
189
|
const artifacts = {};
|
|
190
190
|
const pageLoadErrorId = `pageLoadError-${navigationContext.navigation.id}`;
|
|
191
|
-
if (debugData.devtoolsLog)
|
|
192
|
-
|
|
191
|
+
if (debugData.devtoolsLog) {
|
|
192
|
+
artifacts.DevtoolsLogError = debugData.devtoolsLog;
|
|
193
|
+
artifacts.devtoolsLogs = {[pageLoadErrorId]: debugData.devtoolsLog};
|
|
194
|
+
}
|
|
195
|
+
if (debugData.trace) {
|
|
196
|
+
artifacts.TraceError = debugData.trace;
|
|
197
|
+
artifacts.traces = {[pageLoadErrorId]: debugData.trace};
|
|
198
|
+
}
|
|
193
199
|
|
|
194
200
|
return {
|
|
195
201
|
pageLoadError,
|
package/core/lib/asset-saver.js
CHANGED
|
@@ -62,6 +62,9 @@ function loadArtifacts(basePath) {
|
|
|
62
62
|
if (passName === 'defaultPass') {
|
|
63
63
|
artifacts.DevtoolsLog = devtoolsLog;
|
|
64
64
|
}
|
|
65
|
+
if (passName === 'pageLoadError-defaultPass') {
|
|
66
|
+
artifacts.DevtoolsLogError = devtoolsLog;
|
|
67
|
+
}
|
|
65
68
|
});
|
|
66
69
|
|
|
67
70
|
// load traces
|
|
@@ -74,6 +77,9 @@ function loadArtifacts(basePath) {
|
|
|
74
77
|
if (passName === 'defaultPass') {
|
|
75
78
|
artifacts.Trace = artifacts.traces[passName];
|
|
76
79
|
}
|
|
80
|
+
if (passName === 'pageLoadError-defaultPass') {
|
|
81
|
+
artifacts.TraceError = artifacts.traces[passName];
|
|
82
|
+
}
|
|
77
83
|
});
|
|
78
84
|
|
|
79
85
|
if (Array.isArray(artifacts.Timing)) {
|
|
@@ -489,7 +489,7 @@ class NetworkRequest {
|
|
|
489
489
|
// Bail if there was no totalTime.
|
|
490
490
|
if (!totalHeader) return;
|
|
491
491
|
|
|
492
|
-
|
|
492
|
+
let totalMs = parseInt(totalHeader.value);
|
|
493
493
|
const TCPMsHeader = this.responseHeaders.find(item => item.name === HEADER_TCP);
|
|
494
494
|
const SSLMsHeader = this.responseHeaders.find(item => item.name === HEADER_SSL);
|
|
495
495
|
const requestMsHeader = this.responseHeaders.find(item => item.name === HEADER_REQ);
|
|
@@ -502,11 +502,20 @@ class NetworkRequest {
|
|
|
502
502
|
const requestMs = requestMsHeader ? Math.max(0, parseInt(requestMsHeader.value)) : 0;
|
|
503
503
|
const responseMs = responseMsHeader ? Math.max(0, parseInt(responseMsHeader.value)) : 0;
|
|
504
504
|
|
|
505
|
-
|
|
506
|
-
if (TCPMs + requestMs + responseMs !== totalMs) {
|
|
505
|
+
if (Number.isNaN(TCPMs + requestMs + responseMs + totalMs)) {
|
|
507
506
|
return;
|
|
508
507
|
}
|
|
509
508
|
|
|
509
|
+
// If things don't add up, tweak the total a bit.
|
|
510
|
+
if (TCPMs + requestMs + responseMs !== totalMs) {
|
|
511
|
+
const delta = Math.abs(TCPMs + requestMs + responseMs - totalMs);
|
|
512
|
+
// We didn't see total being more than 5ms less than the total of the components.
|
|
513
|
+
// Allow some discrepancy in the timing, but not too much.
|
|
514
|
+
if (delta >= 25) return;
|
|
515
|
+
|
|
516
|
+
totalMs = TCPMs + requestMs + responseMs;
|
|
517
|
+
}
|
|
518
|
+
|
|
510
519
|
// Bail if SSL time is > TCP time.
|
|
511
520
|
if (SSLMs > TCPMs) {
|
|
512
521
|
return;
|
package/core/runner.js
CHANGED
|
@@ -367,6 +367,8 @@ class Runner {
|
|
|
367
367
|
|
|
368
368
|
let auditResult;
|
|
369
369
|
try {
|
|
370
|
+
if (artifacts.PageLoadError) throw artifacts.PageLoadError;
|
|
371
|
+
|
|
370
372
|
// Return an early error if an artifact required for the audit is missing or an error.
|
|
371
373
|
for (const artifactName of audit.meta.requiredArtifacts) {
|
|
372
374
|
const noArtifact = artifacts[artifactName] === undefined;
|
package/package.json
CHANGED
package/types/artifacts.d.ts
CHANGED
|
@@ -108,6 +108,8 @@ export interface GathererArtifacts extends PublicGathererArtifacts {
|
|
|
108
108
|
CSSUsage: {rules: Crdp.CSS.RuleUsage[], stylesheets: Artifacts.CSSStyleSheetInfo[]};
|
|
109
109
|
/** The primary log of devtools protocol activity. */
|
|
110
110
|
DevtoolsLog: DevtoolsLog;
|
|
111
|
+
/** The log of devtools protocol activity if there was a page load error and Chrome navigated to a `chrome-error://` page. */
|
|
112
|
+
DevtoolsLogError: DevtoolsLog;
|
|
111
113
|
/** Information on the document's doctype(or null if not present), specifically the name, publicId, and systemId.
|
|
112
114
|
All properties default to an empty string if not present */
|
|
113
115
|
Doctype: Artifacts.Doctype | null;
|
|
@@ -152,6 +154,8 @@ export interface GathererArtifacts extends PublicGathererArtifacts {
|
|
|
152
154
|
TapTargets: Artifacts.TapTarget[];
|
|
153
155
|
/** The primary trace taken over the entire run. */
|
|
154
156
|
Trace: Trace;
|
|
157
|
+
/** The trace if there was a page load error and Chrome navigated to a `chrome-error://` page. */
|
|
158
|
+
TraceError: Trace;
|
|
155
159
|
/** Elements associated with metrics (ie: Largest Contentful Paint element). */
|
|
156
160
|
TraceElements: Artifacts.TraceElement[];
|
|
157
161
|
/** Parsed version of the page's Web App Manifest, or null if none found. */
|