lighthouse 11.3.0-dev.20231206 → 11.3.0-dev.20231208
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/bf-cache.d.ts +1 -0
- package/core/audits/bf-cache.js +11 -1
- package/core/audits/bootup-time.js +3 -0
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +6 -4
- package/core/audits/byte-efficiency/duplicated-javascript.d.ts +0 -6
- package/core/audits/byte-efficiency/duplicated-javascript.js +9 -37
- package/core/audits/byte-efficiency/legacy-javascript.js +1 -1
- package/core/audits/byte-efficiency/unminified-javascript.js +2 -2
- package/core/audits/byte-efficiency/unused-javascript.js +10 -10
- package/core/audits/layout-shift-elements.js +23 -12
- package/core/audits/metrics/cumulative-layout-shift.js +5 -1
- package/core/audits/resource-summary.d.ts +11 -0
- package/core/audits/resource-summary.js +86 -0
- package/core/audits/third-party-cookies.d.ts +22 -0
- package/core/audits/third-party-cookies.js +112 -0
- package/core/computed/metrics/cumulative-layout-shift.d.ts +13 -1
- package/core/computed/metrics/cumulative-layout-shift.js +47 -1
- package/core/computed/metrics/lantern-first-contentful-paint.d.ts +50 -16
- package/core/computed/metrics/lantern-first-contentful-paint.js +56 -59
- package/core/computed/metrics/lantern-first-meaningful-paint.js +10 -12
- package/core/computed/metrics/lantern-largest-contentful-paint.js +9 -12
- package/core/computed/metrics/lantern-metric.d.ts +2 -2
- package/core/computed/metrics/lantern-metric.js +6 -5
- package/core/config/default-config.js +4 -0
- package/core/config/filters.js +1 -0
- package/core/gather/base-artifacts.js +2 -1
- package/core/gather/gatherers/css-usage.js +5 -0
- package/core/gather/gatherers/trace-elements.d.ts +6 -15
- package/core/gather/gatherers/trace-elements.js +15 -71
- package/core/lib/i18n/i18n.d.ts +2 -2
- package/core/lib/i18n/i18n.js +2 -2
- package/core/lib/network-request.js +4 -1
- package/core/lib/rect-helpers.d.ts +5 -0
- package/core/lib/rect-helpers.js +15 -0
- package/core/lib/script-helpers.d.ts +1 -1
- package/core/lib/script-helpers.js +1 -1
- package/dist/report/bundle.esm.js +3 -3
- package/dist/report/flow.js +5 -5
- package/dist/report/standalone.js +4 -4
- package/package.json +1 -1
- package/report/renderer/performance-category-renderer.js +4 -2
- package/shared/localization/locales/en-US.json +15 -0
- package/shared/localization/locales/en-XL.json +15 -0
- package/tsconfig.json +1 -0
- package/types/artifacts.d.ts +3 -2
package/core/audits/bf-cache.js
CHANGED
|
@@ -26,6 +26,8 @@ const UIStrings = {
|
|
|
26
26
|
failureReasonColumn: 'Failure reason',
|
|
27
27
|
/** Label for a column in a data table; entries in the column will be a string representing the type of failure preventing the back/forward cache from being used. */
|
|
28
28
|
failureTypeColumn: 'Failure type',
|
|
29
|
+
/** Warning explaining that the back/forward cache results cannot be shown in the old Headless Chrome. "back/forward" refers to the back and forward buttons found in modern browsers. "Headless Chrome" is a product name and should not be translated. */
|
|
30
|
+
warningHeadless: 'Back/forward cache cannot be tested in old Headless Chrome (`--chrome-flags="--headless=old"`). To see audit results, use the new Headless Chrome (`--chrome-flags="--headless=new"`) or standard Chrome.',
|
|
29
31
|
/**
|
|
30
32
|
* @description [ICU Syntax] Label for an audit identifying the number of back/forward cache failure reasons found in the page.
|
|
31
33
|
*/
|
|
@@ -60,7 +62,7 @@ class BFCache extends Audit {
|
|
|
60
62
|
description: str_(UIStrings.description),
|
|
61
63
|
supportedModes: ['navigation', 'timespan'],
|
|
62
64
|
guidanceLevel: 2,
|
|
63
|
-
requiredArtifacts: ['BFCacheFailures'],
|
|
65
|
+
requiredArtifacts: ['BFCacheFailures', 'HostProduct'],
|
|
64
66
|
scoreDisplayMode: Audit.SCORING_MODES.BINARY,
|
|
65
67
|
};
|
|
66
68
|
}
|
|
@@ -70,6 +72,14 @@ class BFCache extends Audit {
|
|
|
70
72
|
* @return {Promise<LH.Audit.Product>}
|
|
71
73
|
*/
|
|
72
74
|
static async audit(artifacts) {
|
|
75
|
+
if (/HeadlessChrome/.test(artifacts.HostProduct)) {
|
|
76
|
+
return {
|
|
77
|
+
score: null,
|
|
78
|
+
notApplicable: true,
|
|
79
|
+
warnings: [str_(UIStrings.warningHeadless)],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
73
83
|
const failures = artifacts.BFCacheFailures;
|
|
74
84
|
if (!failures.length) return {score: 1};
|
|
75
85
|
|
|
@@ -109,6 +109,9 @@ class BootupTime extends Audit {
|
|
|
109
109
|
settings.throttling.cpuSlowdownMultiplier : 1;
|
|
110
110
|
|
|
111
111
|
const executionTimings = getExecutionTimingsByURL(tasks, networkRecords);
|
|
112
|
+
// Exclude our own tasks.
|
|
113
|
+
executionTimings.delete('_lighthouse-eval.js');
|
|
114
|
+
|
|
112
115
|
const tbtImpact = await this.getTbtImpact(artifacts, context);
|
|
113
116
|
|
|
114
117
|
let hadExcessiveChromeExtension = false;
|
|
@@ -206,10 +206,10 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
206
206
|
if (metricComputationInput.gatherContext.gatherMode === 'navigation') {
|
|
207
207
|
const graph = await PageDependencyGraph.request(metricComputationInput, context);
|
|
208
208
|
const {
|
|
209
|
-
|
|
209
|
+
optimisticGraph: optimisticFCPGraph,
|
|
210
210
|
} = await LanternFirstContentfulPaint.request(metricComputationInput, context);
|
|
211
211
|
const {
|
|
212
|
-
|
|
212
|
+
optimisticGraph: optimisticLCPGraph,
|
|
213
213
|
} = await LanternLargestContentfulPaint.request(metricComputationInput, context);
|
|
214
214
|
|
|
215
215
|
wastedMs = this.computeWasteWithTTIGraph(results, graph, simulator, {
|
|
@@ -218,17 +218,19 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
218
218
|
|
|
219
219
|
const {savings: fcpSavings} = this.computeWasteWithGraph(
|
|
220
220
|
results,
|
|
221
|
-
|
|
221
|
+
optimisticFCPGraph,
|
|
222
222
|
simulator,
|
|
223
223
|
{providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'fcp'}
|
|
224
224
|
);
|
|
225
|
+
// Note: LCP's optimistic graph sometimes unexpectedly yields higher savings than the pessimistic graph.
|
|
225
226
|
const {savings: lcpGraphSavings} = this.computeWasteWithGraph(
|
|
226
227
|
results,
|
|
227
|
-
|
|
228
|
+
optimisticLCPGraph,
|
|
228
229
|
simulator,
|
|
229
230
|
{providedWastedBytesByUrl: result.wastedBytesByUrl, label: 'lcp'}
|
|
230
231
|
);
|
|
231
232
|
|
|
233
|
+
|
|
232
234
|
// The LCP graph can underestimate the LCP savings if there is potential savings on the LCP record itself.
|
|
233
235
|
let lcpRecordSavings = 0;
|
|
234
236
|
const lcpRecord = await LCPImageRecord.request(metricComputationInput, context);
|
|
@@ -25,12 +25,6 @@ declare class DuplicatedJavascript extends ByteEfficiencyAudit {
|
|
|
25
25
|
scriptUrl: string;
|
|
26
26
|
resourceSize: number;
|
|
27
27
|
}[]>>;
|
|
28
|
-
/**
|
|
29
|
-
*
|
|
30
|
-
* @param {LH.Artifacts.NetworkRequest|undefined} networkRecord
|
|
31
|
-
* @param {number} contentLength
|
|
32
|
-
*/
|
|
33
|
-
static _estimateTransferRatio(networkRecord: LH.Artifacts.NetworkRequest | undefined, contentLength: number): number;
|
|
34
28
|
/**
|
|
35
29
|
* This audit highlights JavaScript modules that appear to be duplicated across all resources,
|
|
36
30
|
* either within the same bundle or between different bundles. Each details item returned is
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
|
|
12
12
|
import {ModuleDuplication} from '../../computed/module-duplication.js';
|
|
13
13
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
14
|
-
import {
|
|
14
|
+
import {estimateCompressionRatioForContent} from '../../lib/script-helpers.js';
|
|
15
15
|
|
|
16
16
|
const UIStrings = {
|
|
17
17
|
/** Imperative title of a Lighthouse audit that tells the user to remove duplicate JavaScript from their code. This is displayed in a list of audit titles that Lighthouse generates. */
|
|
@@ -103,16 +103,6 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
|
|
|
103
103
|
return groupedDuplication;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
* @param {LH.Artifacts.NetworkRequest|undefined} networkRecord
|
|
109
|
-
* @param {number} contentLength
|
|
110
|
-
*/
|
|
111
|
-
static _estimateTransferRatio(networkRecord, contentLength) {
|
|
112
|
-
const transferSize = estimateTransferSize(networkRecord, contentLength, 'Script');
|
|
113
|
-
return transferSize / contentLength;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
106
|
/**
|
|
117
107
|
* This audit highlights JavaScript modules that appear to be duplicated across all resources,
|
|
118
108
|
* either within the same bundle or between different bundles. Each details item returned is
|
|
@@ -131,7 +121,7 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
|
|
|
131
121
|
await DuplicatedJavascript._getDuplicationGroupedByNodeModules(artifacts, context);
|
|
132
122
|
|
|
133
123
|
/** @type {Map<string, number>} */
|
|
134
|
-
const
|
|
124
|
+
const compressionRatioByUrl = new Map();
|
|
135
125
|
|
|
136
126
|
/** @type {Item[]} */
|
|
137
127
|
const items = [];
|
|
@@ -144,10 +134,10 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
|
|
|
144
134
|
for (const [source, sourceDatas] of duplication.entries()) {
|
|
145
135
|
// One copy of this module is treated as the canonical version - the rest will have
|
|
146
136
|
// non-zero `wastedBytes`. In the case of all copies being the same version, all sizes are
|
|
147
|
-
// equal and the selection doesn't matter. When the copies are
|
|
148
|
-
// matter. Ideally the newest version would be the canonical
|
|
149
|
-
// is not present. Instead, size is used as a heuristic for
|
|
150
|
-
// audit conserative in its estimation.
|
|
137
|
+
// equal and the selection doesn't matter (ignoring compression ratios). When the copies are
|
|
138
|
+
// different versions, it does matter. Ideally the newest version would be the canonical
|
|
139
|
+
// copy, but version information is not present. Instead, size is used as a heuristic for
|
|
140
|
+
// latest version. This makes the audit conserative in its estimation.
|
|
151
141
|
|
|
152
142
|
/** @type {SubItem[]} */
|
|
153
143
|
const subItems = [];
|
|
@@ -158,27 +148,9 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
|
|
|
158
148
|
const scriptId = sourceData.scriptId;
|
|
159
149
|
const script = artifacts.Scripts.find(script => script.scriptId === scriptId);
|
|
160
150
|
const url = script?.url || '';
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (transferRatio === undefined) {
|
|
165
|
-
if (!script || script.length === undefined) {
|
|
166
|
-
// This should never happen because we found the wasted bytes from bundles, which required contents in a Script.
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const contentLength = script.length;
|
|
171
|
-
const networkRecord = getRequestForScript(networkRecords, script);
|
|
172
|
-
transferRatio = DuplicatedJavascript._estimateTransferRatio(networkRecord, contentLength);
|
|
173
|
-
transferRatioByUrl.set(url, transferRatio);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (transferRatio === undefined) {
|
|
177
|
-
// Shouldn't happen for above reasons.
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const transferSize = Math.round(sourceData.resourceSize * transferRatio);
|
|
151
|
+
const compressionRatio = estimateCompressionRatioForContent(
|
|
152
|
+
compressionRatioByUrl, url, artifacts, networkRecords);
|
|
153
|
+
const transferSize = Math.round(sourceData.resourceSize * compressionRatio);
|
|
182
154
|
|
|
183
155
|
subItems.push({
|
|
184
156
|
url,
|
|
@@ -417,7 +417,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
417
417
|
const scriptToMatchResults =
|
|
418
418
|
this.detectAcrossScripts(matcher, artifacts.Scripts, networkRecords, bundles);
|
|
419
419
|
for (const [script, matches] of scriptToMatchResults.entries()) {
|
|
420
|
-
const compressionRatio =
|
|
420
|
+
const compressionRatio = estimateCompressionRatioForContent(
|
|
421
421
|
compressionRatioByUrl, script.url, artifacts, networkRecords);
|
|
422
422
|
const wastedBytes = Math.round(this.estimateWastedBytes(matches) * compressionRatio);
|
|
423
423
|
/** @type {typeof items[number]} */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
|
|
8
8
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
9
9
|
import {computeJSTokenLength as computeTokenLength} from '../../lib/minification-estimator.js';
|
|
10
|
-
import {
|
|
10
|
+
import {estimateCompressedContentSize, getRequestForScript, isInline} from '../../lib/script-helpers.js';
|
|
11
11
|
import {Util} from '../../../shared/util.js';
|
|
12
12
|
|
|
13
13
|
const UIStrings = {
|
|
@@ -58,7 +58,7 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
|
|
|
58
58
|
const contentLength = scriptContent.length;
|
|
59
59
|
const totalTokenLength = computeTokenLength(scriptContent);
|
|
60
60
|
|
|
61
|
-
const totalBytes =
|
|
61
|
+
const totalBytes = estimateCompressedContentSize(networkRecord, contentLength, 'Script');
|
|
62
62
|
const wastedRatio = 1 - totalTokenLength / contentLength;
|
|
63
63
|
const wastedBytes = Math.round(totalBytes * wastedRatio);
|
|
64
64
|
|
|
@@ -8,7 +8,7 @@ import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
|
|
|
8
8
|
import {UnusedJavascriptSummary} from '../../computed/unused-javascript-summary.js';
|
|
9
9
|
import {JSBundles} from '../../computed/js-bundles.js';
|
|
10
10
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
11
|
-
import {
|
|
11
|
+
import {estimateCompressionRatioForContent} from '../../lib/script-helpers.js';
|
|
12
12
|
|
|
13
13
|
const UIStrings = {
|
|
14
14
|
/** Imperative title of a Lighthouse audit that tells the user to reduce JavaScript that is never evaluated during page load. This is displayed in a list of audit titles that Lighthouse generates. */
|
|
@@ -86,26 +86,26 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
|
|
|
86
86
|
bundleSourceUnusedThreshold = UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_THRESHOLD,
|
|
87
87
|
} = context.options || {};
|
|
88
88
|
|
|
89
|
+
/** @type {Map<string, number>} */
|
|
90
|
+
const compressionRatioByUrl = new Map();
|
|
91
|
+
|
|
89
92
|
const items = [];
|
|
90
93
|
for (const [scriptId, scriptCoverage] of Object.entries(artifacts.JsUsage)) {
|
|
91
94
|
const script = artifacts.Scripts.find(s => s.scriptId === scriptId);
|
|
92
95
|
if (!script) continue; // This should never happen.
|
|
93
96
|
|
|
94
|
-
const networkRecord = getRequestForScript(networkRecords, script);
|
|
95
|
-
if (!networkRecord) continue;
|
|
96
|
-
|
|
97
97
|
const bundle = bundles.find(b => b.script.scriptId === scriptId);
|
|
98
98
|
const unusedJsSummary =
|
|
99
99
|
await UnusedJavascriptSummary.request({scriptId, scriptCoverage, bundle}, context);
|
|
100
100
|
if (unusedJsSummary.wastedBytes === 0 || unusedJsSummary.totalBytes === 0) continue;
|
|
101
101
|
|
|
102
|
-
const
|
|
103
|
-
|
|
102
|
+
const compressionRatio = estimateCompressionRatioForContent(
|
|
103
|
+
compressionRatioByUrl, script.url, artifacts, networkRecords);
|
|
104
104
|
/** @type {LH.Audit.ByteEfficiencyItem} */
|
|
105
105
|
const item = {
|
|
106
106
|
url: script.url,
|
|
107
|
-
totalBytes: Math.round(
|
|
108
|
-
wastedBytes: Math.round(
|
|
107
|
+
totalBytes: Math.round(compressionRatio * unusedJsSummary.totalBytes),
|
|
108
|
+
wastedBytes: Math.round(compressionRatio * unusedJsSummary.wastedBytes),
|
|
109
109
|
wastedPercent: unusedJsSummary.wastedPercent,
|
|
110
110
|
};
|
|
111
111
|
|
|
@@ -126,8 +126,8 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
|
|
|
126
126
|
const total = source === '(unmapped)' ? sizes.unmappedBytes : sizes.files[source];
|
|
127
127
|
return {
|
|
128
128
|
source,
|
|
129
|
-
unused: Math.round(unused *
|
|
130
|
-
total: Math.round(total *
|
|
129
|
+
unused: Math.round(unused * compressionRatio),
|
|
130
|
+
total: Math.round(total * compressionRatio),
|
|
131
131
|
};
|
|
132
132
|
})
|
|
133
133
|
.filter(d => d.unused >= bundleSourceUnusedThreshold);
|
|
@@ -40,15 +40,28 @@ class LayoutShiftElements extends Audit {
|
|
|
40
40
|
* @return {Promise<LH.Audit.Product>}
|
|
41
41
|
*/
|
|
42
42
|
static async audit(artifacts, context) {
|
|
43
|
-
const
|
|
44
|
-
.
|
|
43
|
+
const {cumulativeLayoutShift: clsSavings, impactByNodeId} =
|
|
44
|
+
await CumulativeLayoutShiftComputed.request(artifacts.traces[Audit.DEFAULT_PASS], context);
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
/** @type {Array<{node: LH.Audit.Details.ItemValue, score: number}>} */
|
|
47
|
+
const clsElementData = artifacts.TraceElements
|
|
48
|
+
.filter(element => element.traceEventType === 'layout-shift')
|
|
49
|
+
.map(element => ({
|
|
48
50
|
node: Audit.makeNodeItem(element.node),
|
|
49
|
-
score: element.
|
|
50
|
-
};
|
|
51
|
-
|
|
51
|
+
score: impactByNodeId.get(element.nodeId) || 0,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
if (clsElementData.length < impactByNodeId.size) {
|
|
55
|
+
const elementDataImpact = clsElementData.reduce((sum, {score}) => sum += score || 0, 0);
|
|
56
|
+
const remainingImpact = Math.max(clsSavings - elementDataImpact, 0);
|
|
57
|
+
clsElementData.push({
|
|
58
|
+
node: {
|
|
59
|
+
type: 'code',
|
|
60
|
+
value: str_(i18n.UIStrings.otherResourceType),
|
|
61
|
+
},
|
|
62
|
+
score: remainingImpact,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
52
65
|
|
|
53
66
|
/** @type {LH.Audit.Details.Table['headings']} */
|
|
54
67
|
const headings = [
|
|
@@ -58,15 +71,13 @@ class LayoutShiftElements extends Audit {
|
|
|
58
71
|
];
|
|
59
72
|
|
|
60
73
|
const details = Audit.makeTableDetails(headings, clsElementData);
|
|
74
|
+
|
|
61
75
|
let displayValue;
|
|
62
|
-
if (
|
|
76
|
+
if (impactByNodeId.size > 0) {
|
|
63
77
|
displayValue = str_(i18n.UIStrings.displayValueElementsFound,
|
|
64
|
-
{nodeCount:
|
|
78
|
+
{nodeCount: impactByNodeId.size});
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
const {cumulativeLayoutShift: clsSavings} =
|
|
68
|
-
await CumulativeLayoutShiftComputed.request(artifacts.traces[Audit.DEFAULT_PASS], context);
|
|
69
|
-
|
|
70
81
|
const passed = clsSavings <= CumulativeLayoutShift.defaultOptions.p10;
|
|
71
82
|
|
|
72
83
|
return {
|
|
@@ -54,7 +54,11 @@ class CumulativeLayoutShift extends Audit {
|
|
|
54
54
|
*/
|
|
55
55
|
static async audit(artifacts, context) {
|
|
56
56
|
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
57
|
-
|
|
57
|
+
|
|
58
|
+
// impactByNodeId is unused but we don't want it on debug data
|
|
59
|
+
// eslint-disable-next-line no-unused-vars
|
|
60
|
+
const {cumulativeLayoutShift, impactByNodeId, ...rest} =
|
|
61
|
+
await ComputedCLS.request(trace, context);
|
|
58
62
|
|
|
59
63
|
/** @type {LH.Audit.Details.DebugData} */
|
|
60
64
|
const details = {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default ResourceSummary;
|
|
2
|
+
declare class ResourceSummary extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* @param {LH.Artifacts} artifacts
|
|
5
|
+
* @param {LH.Audit.Context} context
|
|
6
|
+
* @return {Promise<LH.Audit.Product>}
|
|
7
|
+
*/
|
|
8
|
+
static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
9
|
+
}
|
|
10
|
+
import { Audit } from './audit.js';
|
|
11
|
+
//# sourceMappingURL=resource-summary.d.ts.map
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2019 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
|
+
import {Audit} from './audit.js';
|
|
8
|
+
import {ResourceSummary as ComputedResourceSummary} from '../computed/resource-summary.js';
|
|
9
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
10
|
+
|
|
11
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url);
|
|
12
|
+
|
|
13
|
+
class ResourceSummary extends Audit {
|
|
14
|
+
/**
|
|
15
|
+
* @return {LH.Audit.Meta}
|
|
16
|
+
*/
|
|
17
|
+
static get meta() {
|
|
18
|
+
return {
|
|
19
|
+
id: 'resource-summary',
|
|
20
|
+
title: 'Resources Summary',
|
|
21
|
+
description: 'Aggregates all network requests and groups them by type',
|
|
22
|
+
scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
|
|
23
|
+
requiredArtifacts: ['devtoolsLogs', 'URL'],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {LH.Artifacts} artifacts
|
|
29
|
+
* @param {LH.Audit.Context} context
|
|
30
|
+
* @return {Promise<LH.Audit.Product>}
|
|
31
|
+
*/
|
|
32
|
+
static async audit(artifacts, context) {
|
|
33
|
+
const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
|
|
34
|
+
const summary = await ComputedResourceSummary
|
|
35
|
+
.request({devtoolsLog, URL: artifacts.URL, budgets: context.settings.budgets}, context);
|
|
36
|
+
|
|
37
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
38
|
+
const headings = [
|
|
39
|
+
{key: 'label', valueType: 'text', label: str_(i18n.UIStrings.columnResourceType)},
|
|
40
|
+
{key: 'requestCount', valueType: 'numeric', label: str_(i18n.UIStrings.columnRequests)},
|
|
41
|
+
{key: 'transferSize', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize)},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
/** @type {Record<LH.Budget.ResourceType, LH.IcuMessage>} */
|
|
46
|
+
const strMappings = {
|
|
47
|
+
'total': str_(i18n.UIStrings.totalResourceType),
|
|
48
|
+
'document': str_(i18n.UIStrings.documentResourceType),
|
|
49
|
+
'script': str_(i18n.UIStrings.scriptResourceType),
|
|
50
|
+
'stylesheet': str_(i18n.UIStrings.stylesheetResourceType),
|
|
51
|
+
'image': str_(i18n.UIStrings.imageResourceType),
|
|
52
|
+
'media': str_(i18n.UIStrings.mediaResourceType),
|
|
53
|
+
'font': str_(i18n.UIStrings.fontResourceType),
|
|
54
|
+
'other': str_(i18n.UIStrings.otherResourceType),
|
|
55
|
+
'third-party': str_(i18n.UIStrings.thirdPartyResourceType),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const types = /** @type {Array<LH.Budget.ResourceType>} */ (Object.keys(summary));
|
|
59
|
+
const rows = types.map(type => {
|
|
60
|
+
return {
|
|
61
|
+
// ResourceType is included as an "id" for ease of use.
|
|
62
|
+
// It does not appear directly in the table.
|
|
63
|
+
resourceType: type,
|
|
64
|
+
label: strMappings[type],
|
|
65
|
+
requestCount: summary[type].count,
|
|
66
|
+
transferSize: summary[type].transferSize,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
// Force third-party to be last, descending by size otherwise
|
|
70
|
+
const thirdPartyRow = rows.find(r => r.resourceType === 'third-party') || [];
|
|
71
|
+
const otherRows = rows.filter(r => r.resourceType !== 'third-party')
|
|
72
|
+
.sort((a, b) => {
|
|
73
|
+
return b.transferSize - a.transferSize;
|
|
74
|
+
});
|
|
75
|
+
const tableItems = otherRows.concat(thirdPartyRow);
|
|
76
|
+
|
|
77
|
+
const tableDetails = Audit.makeTableDetails(headings, tableItems);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
details: tableDetails,
|
|
81
|
+
score: null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default ResourceSummary;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default ThirdPartyCookies;
|
|
2
|
+
declare class ThirdPartyCookies extends Audit {
|
|
3
|
+
/**
|
|
4
|
+
* https://source.chromium.org/chromium/chromium/src/+/d2fcd4ba302baeabf4b96d8fa9fdb7a215736c31:third_party/devtools-frontend/src/front_end/models/issues_manager/CookieIssue.ts;l=62-69
|
|
5
|
+
* @param {LH.Crdp.Audits.CookieIssueDetails} cookieIssue
|
|
6
|
+
* @return {string}
|
|
7
|
+
*/
|
|
8
|
+
static getCookieId(cookieIssue: LH.Crdp.Audits.CookieIssueDetails): string;
|
|
9
|
+
/**
|
|
10
|
+
* @param {LH.Artifacts} artifacts
|
|
11
|
+
* @return {Promise<LH.Audit.Product>}
|
|
12
|
+
*/
|
|
13
|
+
static audit(artifacts: LH.Artifacts): Promise<LH.Audit.Product>;
|
|
14
|
+
}
|
|
15
|
+
export namespace UIStrings {
|
|
16
|
+
const title: string;
|
|
17
|
+
const failureTitle: string;
|
|
18
|
+
const description: string;
|
|
19
|
+
const displayValue: string;
|
|
20
|
+
}
|
|
21
|
+
import { Audit } from './audit.js';
|
|
22
|
+
//# sourceMappingURL=third-party-cookies.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2023 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Audits a page to determine if it is using third party cookies.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {Audit} from './audit.js';
|
|
12
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
13
|
+
|
|
14
|
+
/* eslint-disable max-len */
|
|
15
|
+
const UIStrings = {
|
|
16
|
+
/** Title of a Lighthouse audit that provides detail on the use of third party cookies. This descriptive title is shown to users when the page does not use third party cookies. */
|
|
17
|
+
title: 'Avoids third-party cookies',
|
|
18
|
+
/** Title of a Lighthouse audit that provides detail on the use of third party cookies. This descriptive title is shown to users when the page uses third party cookies. */
|
|
19
|
+
failureTitle: 'Uses third-party cookies',
|
|
20
|
+
/** Description of a Lighthouse audit that tells the user why they should not use third party cookies on their page. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
|
|
21
|
+
description: 'Support for third-party cookies will be removed in a future version of Chrome. [Learn more about phasing out third-party cookies](https://developer.chrome.com/en/docs/privacy-sandbox/third-party-cookie-phase-out/).',
|
|
22
|
+
/** [ICU Syntax] Label for the audit identifying the number of third-party cookies. */
|
|
23
|
+
displayValue: `{itemCount, plural,
|
|
24
|
+
=1 {1 cookie found}
|
|
25
|
+
other {# cookies found}
|
|
26
|
+
}`,
|
|
27
|
+
};
|
|
28
|
+
/* eslint-enable max-len */
|
|
29
|
+
|
|
30
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
31
|
+
|
|
32
|
+
class ThirdPartyCookies extends Audit {
|
|
33
|
+
/**
|
|
34
|
+
* @return {LH.Audit.Meta}
|
|
35
|
+
*/
|
|
36
|
+
static get meta() {
|
|
37
|
+
return {
|
|
38
|
+
id: 'third-party-cookies',
|
|
39
|
+
title: str_(UIStrings.title),
|
|
40
|
+
failureTitle: str_(UIStrings.failureTitle),
|
|
41
|
+
description: str_(UIStrings.description),
|
|
42
|
+
requiredArtifacts: ['InspectorIssues'],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* https://source.chromium.org/chromium/chromium/src/+/d2fcd4ba302baeabf4b96d8fa9fdb7a215736c31:third_party/devtools-frontend/src/front_end/models/issues_manager/CookieIssue.ts;l=62-69
|
|
48
|
+
* @param {LH.Crdp.Audits.CookieIssueDetails} cookieIssue
|
|
49
|
+
* @return {string}
|
|
50
|
+
*/
|
|
51
|
+
static getCookieId(cookieIssue) {
|
|
52
|
+
if (!cookieIssue.cookie) {
|
|
53
|
+
return cookieIssue.rawCookieLine ?? 'no-cookie-info';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const {domain, path, name} = cookieIssue.cookie;
|
|
57
|
+
const cookieId = `${domain};${path};${name}`;
|
|
58
|
+
return cookieId;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {LH.Artifacts} artifacts
|
|
63
|
+
* @return {Promise<LH.Audit.Product>}
|
|
64
|
+
*/
|
|
65
|
+
static async audit(artifacts) {
|
|
66
|
+
/** @type {Set<string>} */
|
|
67
|
+
const seenCookies = new Set();
|
|
68
|
+
|
|
69
|
+
/** @type {LH.Audit.Details.TableItem[]} */
|
|
70
|
+
const items = [];
|
|
71
|
+
for (const issue of artifacts.InspectorIssues.cookieIssue) {
|
|
72
|
+
const isPhaseoutWarn = issue.cookieWarningReasons.includes('WarnThirdPartyPhaseout');
|
|
73
|
+
const isPhaseoutExclude = issue.cookieExclusionReasons.includes('ExcludeThirdPartyPhaseout');
|
|
74
|
+
if (!isPhaseoutWarn && !isPhaseoutExclude) continue;
|
|
75
|
+
|
|
76
|
+
// According to JSDOC for `issue.cookie`, if `cookie` is undefined then `rawCookieLine`
|
|
77
|
+
// should be set and no valid cookie could be created. It should be safe to skip in this case.
|
|
78
|
+
const name = issue.cookie?.name || issue.rawCookieLine;
|
|
79
|
+
if (!name) continue;
|
|
80
|
+
|
|
81
|
+
const cookieId = this.getCookieId(issue);
|
|
82
|
+
if (seenCookies.has(cookieId)) continue;
|
|
83
|
+
seenCookies.add(cookieId);
|
|
84
|
+
|
|
85
|
+
items.push({
|
|
86
|
+
name,
|
|
87
|
+
url: issue.cookieUrl,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
92
|
+
const headings = [
|
|
93
|
+
{key: 'name', valueType: 'text', label: str_(i18n.UIStrings.columnName)},
|
|
94
|
+
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
|
|
95
|
+
];
|
|
96
|
+
const details = Audit.makeTableDetails(headings, items);
|
|
97
|
+
|
|
98
|
+
let displayValue;
|
|
99
|
+
if (items.length > 0) {
|
|
100
|
+
displayValue = str_(UIStrings.displayValue, {itemCount: items.length});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
score: items.length ? 0 : 1,
|
|
105
|
+
displayValue,
|
|
106
|
+
details,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default ThirdPartyCookies;
|
|
112
|
+
export {UIStrings};
|
|
@@ -11,6 +11,7 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
|
|
|
11
11
|
}>) => Promise<{
|
|
12
12
|
cumulativeLayoutShift: number;
|
|
13
13
|
cumulativeLayoutShiftMainFrame: number;
|
|
14
|
+
impactByNodeId: Map<number, number>;
|
|
14
15
|
}>;
|
|
15
16
|
};
|
|
16
17
|
declare class CumulativeLayoutShift {
|
|
@@ -23,6 +24,16 @@ declare class CumulativeLayoutShift {
|
|
|
23
24
|
* @return {Array<LayoutShiftEvent>}
|
|
24
25
|
*/
|
|
25
26
|
static getLayoutShiftEvents(processedTrace: LH.Artifacts.ProcessedTrace): Array<LayoutShiftEvent>;
|
|
27
|
+
/**
|
|
28
|
+
* Each layout shift event has a 'score' which is the amount added to the CLS as a result of the given shift(s).
|
|
29
|
+
* We calculate the score per element by taking the 'score' of each layout shift event and
|
|
30
|
+
* distributing it between all the nodes that were shifted, proportianal to the impact region of
|
|
31
|
+
* each shifted element.
|
|
32
|
+
*
|
|
33
|
+
* @param {LayoutShiftEvent[]} layoutShiftEvents
|
|
34
|
+
* @return {Map<number, number>}
|
|
35
|
+
*/
|
|
36
|
+
static getImpactByNodeId(layoutShiftEvents: LayoutShiftEvent[]): Map<number, number>;
|
|
26
37
|
/**
|
|
27
38
|
* Calculates cumulative layout shifts per cluster (session) of LayoutShift
|
|
28
39
|
* events -- where a new cluster is created when there's a gap of more than
|
|
@@ -35,11 +46,12 @@ declare class CumulativeLayoutShift {
|
|
|
35
46
|
/**
|
|
36
47
|
* @param {LH.Trace} trace
|
|
37
48
|
* @param {LH.Artifacts.ComputedContext} context
|
|
38
|
-
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}>}
|
|
49
|
+
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>}>}
|
|
39
50
|
*/
|
|
40
51
|
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<{
|
|
41
52
|
cumulativeLayoutShift: number;
|
|
42
53
|
cumulativeLayoutShiftMainFrame: number;
|
|
54
|
+
impactByNodeId: Map<number, number>;
|
|
43
55
|
}>;
|
|
44
56
|
}
|
|
45
57
|
import { ProcessedTrace } from '../processed-trace.js';
|