lighthouse 11.4.0 → 11.5.0
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/test/smokehouse/__snapshots__/report-assert-test.js.snap +14 -4
- package/cli/test/smokehouse/core-tests.js +2 -0
- package/core/audits/audit.d.ts +5 -0
- package/core/audits/audit.js +46 -2
- package/core/audits/bf-cache.js +1 -1
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +1 -1
- package/core/audits/byte-efficiency/legacy-javascript.js +13 -2
- package/core/audits/byte-efficiency/render-blocking-resources.d.ts +5 -4
- package/core/audits/byte-efficiency/render-blocking-resources.js +15 -9
- package/core/audits/byte-efficiency/unused-css-rules.js +1 -1
- package/core/audits/byte-efficiency/unused-javascript.js +1 -1
- package/core/audits/layout-shift-elements.js +1 -1
- package/core/audits/layout-shifts.d.ts +33 -0
- package/core/audits/layout-shifts.js +158 -0
- package/core/audits/prioritize-lcp-image.js +1 -1
- package/core/audits/unsized-images.js +1 -1
- package/core/audits/viewport.js +10 -0
- package/core/computed/metrics/cumulative-layout-shift.d.ts +20 -1
- package/core/computed/metrics/cumulative-layout-shift.js +74 -4
- package/core/computed/trace-engine-result.d.ts +40 -0
- package/core/computed/trace-engine-result.js +69 -0
- package/core/computed/unused-css.js +4 -4
- package/core/computed/viewport-meta.d.ts +4 -0
- package/core/computed/viewport-meta.js +6 -1
- package/core/config/default-config.js +4 -1
- package/core/config/metrics-to-audits.js +1 -0
- package/core/gather/driver/target-manager.js +10 -1
- package/core/gather/driver/wait-for-condition.js +1 -1
- package/core/gather/gatherers/dobetterweb/response-compression.js +1 -12
- package/core/gather/gatherers/root-causes.d.ts +20 -0
- package/core/gather/gatherers/root-causes.js +133 -0
- package/core/gather/gatherers/trace-elements.d.ts +38 -7
- package/core/gather/gatherers/trace-elements.js +113 -34
- package/core/gather/gatherers/trace.js +6 -3
- package/core/gather/navigation-runner.js +1 -1
- package/core/gather/session.js +16 -3
- package/core/lib/i18n/i18n.js +2 -0
- package/core/lib/lighthouse-compatibility.js +4 -0
- package/core/lib/network-request.js +10 -2
- package/core/lib/polyfill-dom-rect.d.ts +2 -0
- package/core/lib/polyfill-dom-rect.js +111 -0
- package/core/lib/trace-engine.d.ts +7 -0
- package/core/lib/trace-engine.js +19 -0
- package/core/scripts/download-chrome.sh +17 -0
- package/dist/report/bundle.esm.js +14 -10
- package/dist/report/flow.js +15 -11
- package/dist/report/standalone.js +13 -9
- package/flow-report/src/i18n/i18n.d.ts +2 -0
- package/package.json +5 -4
- package/report/assets/styles.css +9 -5
- package/report/renderer/category-renderer.d.ts +5 -12
- package/report/renderer/category-renderer.js +18 -18
- package/report/renderer/components.js +1 -1
- package/report/renderer/performance-category-renderer.d.ts +2 -1
- package/report/renderer/performance-category-renderer.js +90 -69
- package/report/renderer/pwa-category-renderer.js +11 -2
- package/report/renderer/report-utils.d.ts +1 -0
- package/report/renderer/report-utils.js +3 -0
- package/shared/localization/locales/en-US.json +27 -0
- package/shared/localization/locales/en-XL.json +27 -0
- package/types/artifacts.d.ts +10 -1
- package/types/audit.d.ts +9 -1
- package/types/config.d.ts +1 -0
- package/types/lhr/audit-result.d.ts +1 -7
- package/types/trace-engine.d.ts +1516 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`getAssertionReport works (multiple failing) 1`] = `
|
|
4
4
|
"X difference at cumulative-layout-shift audit.details.items.length
|
|
5
5
|
expected: []
|
|
6
|
-
found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444}]
|
|
6
|
+
found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.13570762803819444,\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444},\\"newEngineResultDiffered\\":false}]
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
X difference at cumulative-layout-shift audit.details.blah
|
|
@@ -28,7 +28,12 @@ exports[`getAssertionReport works (multiple failing) 1`] = `
|
|
|
28
28
|
\\"type\\": \\"debugdata\\",
|
|
29
29
|
\\"items\\": [
|
|
30
30
|
{
|
|
31
|
-
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
|
|
31
|
+
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444,
|
|
32
|
+
\\"newEngineResult\\": {
|
|
33
|
+
\\"cumulativeLayoutShift\\": 0.13570762803819444,
|
|
34
|
+
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
|
|
35
|
+
},
|
|
36
|
+
\\"newEngineResultDiffered\\": false
|
|
32
37
|
}
|
|
33
38
|
]
|
|
34
39
|
}
|
|
@@ -38,7 +43,7 @@ exports[`getAssertionReport works (multiple failing) 1`] = `
|
|
|
38
43
|
exports[`getAssertionReport works (trivial failing) 1`] = `
|
|
39
44
|
"X difference at cumulative-layout-shift audit.details.items.length
|
|
40
45
|
expected: []
|
|
41
|
-
found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444}]
|
|
46
|
+
found: [{\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444,\\"newEngineResult\\":{\\"cumulativeLayoutShift\\":0.13570762803819444,\\"cumulativeLayoutShiftMainFrame\\":0.13570762803819444},\\"newEngineResultDiffered\\":false}]
|
|
42
47
|
|
|
43
48
|
found result:
|
|
44
49
|
{
|
|
@@ -58,7 +63,12 @@ exports[`getAssertionReport works (trivial failing) 1`] = `
|
|
|
58
63
|
\\"type\\": \\"debugdata\\",
|
|
59
64
|
\\"items\\": [
|
|
60
65
|
{
|
|
61
|
-
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
|
|
66
|
+
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444,
|
|
67
|
+
\\"newEngineResult\\": {
|
|
68
|
+
\\"cumulativeLayoutShift\\": 0.13570762803819444,
|
|
69
|
+
\\"cumulativeLayoutShiftMainFrame\\": 0.13570762803819444
|
|
70
|
+
},
|
|
71
|
+
\\"newEngineResultDiffered\\": false
|
|
62
72
|
}
|
|
63
73
|
]
|
|
64
74
|
}
|
|
@@ -63,6 +63,7 @@ import seoPassing from './test-definitions/seo-passing.js';
|
|
|
63
63
|
import seoStatus403 from './test-definitions/seo-status-403.js';
|
|
64
64
|
import seoTapTargets from './test-definitions/seo-tap-targets.js';
|
|
65
65
|
import serviceWorkerReloaded from './test-definitions/service-worker-reloaded.js';
|
|
66
|
+
import shiftAttribution from './test-definitions/shift-attribution.js';
|
|
66
67
|
import sourceMaps from './test-definitions/source-maps.js';
|
|
67
68
|
import timing from './test-definitions/timing.js';
|
|
68
69
|
|
|
@@ -127,6 +128,7 @@ const smokeTests = [
|
|
|
127
128
|
seoStatus403,
|
|
128
129
|
seoTapTargets,
|
|
129
130
|
serviceWorkerReloaded,
|
|
131
|
+
shiftAttribution,
|
|
130
132
|
sourceMaps,
|
|
131
133
|
timing,
|
|
132
134
|
];
|
package/core/audits/audit.d.ts
CHANGED
|
@@ -146,6 +146,11 @@ export class Audit {
|
|
|
146
146
|
* @return {number|null}
|
|
147
147
|
*/
|
|
148
148
|
static _normalizeAuditScore(score: number | null, scoreDisplayMode: LH.Audit.ScoreDisplayMode, auditId: string): number | null;
|
|
149
|
+
/**
|
|
150
|
+
* @param {LH.Audit.ProductMetricSavings|undefined} metricSavings
|
|
151
|
+
* @return {LH.Audit.ProductMetricSavings|undefined}
|
|
152
|
+
*/
|
|
153
|
+
static _quantizeMetricSavings(metricSavings: LH.Audit.ProductMetricSavings | undefined): LH.Audit.ProductMetricSavings | undefined;
|
|
149
154
|
/**
|
|
150
155
|
* @param {typeof Audit} audit
|
|
151
156
|
* @param {string | LH.IcuMessage} errorMessage
|
package/core/audits/audit.js
CHANGED
|
@@ -10,6 +10,15 @@ import {Util} from '../../shared/util.js';
|
|
|
10
10
|
|
|
11
11
|
const DEFAULT_PASS = 'defaultPass';
|
|
12
12
|
|
|
13
|
+
/** @type {Record<keyof LH.Audit.ProductMetricSavings, number>} */
|
|
14
|
+
const METRIC_SAVINGS_PRECISION = {
|
|
15
|
+
FCP: 50,
|
|
16
|
+
LCP: 50,
|
|
17
|
+
INP: 50,
|
|
18
|
+
TBT: 50,
|
|
19
|
+
CLS: 0.001,
|
|
20
|
+
};
|
|
21
|
+
|
|
13
22
|
/**
|
|
14
23
|
* @typedef TableOptions
|
|
15
24
|
* @property {number=} wastedMs
|
|
@@ -321,6 +330,10 @@ class Audit {
|
|
|
321
330
|
* @return {number|null}
|
|
322
331
|
*/
|
|
323
332
|
static _normalizeAuditScore(score, scoreDisplayMode, auditId) {
|
|
333
|
+
if (scoreDisplayMode === Audit.SCORING_MODES.INFORMATIVE) {
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
324
337
|
if (scoreDisplayMode !== Audit.SCORING_MODES.BINARY &&
|
|
325
338
|
scoreDisplayMode !== Audit.SCORING_MODES.NUMERIC &&
|
|
326
339
|
scoreDisplayMode !== Audit.SCORING_MODES.METRIC_SAVINGS) {
|
|
@@ -339,6 +352,34 @@ class Audit {
|
|
|
339
352
|
return score;
|
|
340
353
|
}
|
|
341
354
|
|
|
355
|
+
/**
|
|
356
|
+
* @param {LH.Audit.ProductMetricSavings|undefined} metricSavings
|
|
357
|
+
* @return {LH.Audit.ProductMetricSavings|undefined}
|
|
358
|
+
*/
|
|
359
|
+
static _quantizeMetricSavings(metricSavings) {
|
|
360
|
+
if (!metricSavings) return;
|
|
361
|
+
|
|
362
|
+
/** @type {LH.Audit.ProductMetricSavings} */
|
|
363
|
+
const normalizedMetricSavings = {...metricSavings};
|
|
364
|
+
|
|
365
|
+
// eslint-disable-next-line max-len
|
|
366
|
+
for (const key of /** @type {Array<keyof LH.Audit.ProductMetricSavings>} */ (Object.keys(metricSavings))) {
|
|
367
|
+
let value = metricSavings[key];
|
|
368
|
+
if (value === undefined) continue;
|
|
369
|
+
|
|
370
|
+
value = Math.max(value, 0);
|
|
371
|
+
|
|
372
|
+
const precision = METRIC_SAVINGS_PRECISION[key];
|
|
373
|
+
if (precision !== undefined) {
|
|
374
|
+
value = Math.round(value / precision) * precision;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
normalizedMetricSavings[key] = value;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return normalizedMetricSavings;
|
|
381
|
+
}
|
|
382
|
+
|
|
342
383
|
/**
|
|
343
384
|
* @param {typeof Audit} audit
|
|
344
385
|
* @param {string | LH.IcuMessage} errorMessage
|
|
@@ -378,10 +419,13 @@ class Audit {
|
|
|
378
419
|
scoreDisplayMode = product.scoreDisplayMode;
|
|
379
420
|
}
|
|
380
421
|
|
|
422
|
+
const metricSavings = Audit._quantizeMetricSavings(product.metricSavings);
|
|
423
|
+
const hasSomeSavings = Object.values(metricSavings || {}).some(v => v);
|
|
424
|
+
|
|
381
425
|
if (scoreDisplayMode === Audit.SCORING_MODES.METRIC_SAVINGS) {
|
|
382
426
|
if (score && score >= Util.PASS_THRESHOLD) {
|
|
383
427
|
score = 1;
|
|
384
|
-
} else if (
|
|
428
|
+
} else if (hasSomeSavings) {
|
|
385
429
|
score = 0;
|
|
386
430
|
} else {
|
|
387
431
|
score = 0.5;
|
|
@@ -419,7 +463,7 @@ class Audit {
|
|
|
419
463
|
errorStack: product.errorStack,
|
|
420
464
|
warnings: product.warnings,
|
|
421
465
|
scoringOptions: product.scoringOptions,
|
|
422
|
-
metricSavings
|
|
466
|
+
metricSavings,
|
|
423
467
|
|
|
424
468
|
details: product.details,
|
|
425
469
|
guidanceLevel: audit.meta.guidanceLevel,
|
package/core/audits/bf-cache.js
CHANGED
|
@@ -61,7 +61,7 @@ class BFCache extends Audit {
|
|
|
61
61
|
failureTitle: str_(UIStrings.failureTitle),
|
|
62
62
|
description: str_(UIStrings.description),
|
|
63
63
|
supportedModes: ['navigation', 'timespan'],
|
|
64
|
-
guidanceLevel:
|
|
64
|
+
guidanceLevel: 4,
|
|
65
65
|
requiredArtifacts: ['BFCacheFailures', 'HostProduct'],
|
|
66
66
|
scoreDisplayMode: Audit.SCORING_MODES.BINARY,
|
|
67
67
|
};
|
|
@@ -194,7 +194,7 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
194
194
|
|
|
195
195
|
const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
|
|
196
196
|
|
|
197
|
-
/** @type {LH.Audit.
|
|
197
|
+
/** @type {LH.Audit.ProductMetricSavings} */
|
|
198
198
|
const metricSavings = {
|
|
199
199
|
FCP: 0,
|
|
200
200
|
LCP: 0,
|
|
@@ -152,6 +152,12 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
152
152
|
// Object.defineProperty(String.prototype, 'startsWith'
|
|
153
153
|
expression += `|defineProperty\\(${object || 'window'},\\s?${qt(property)}`;
|
|
154
154
|
|
|
155
|
+
// es-shims
|
|
156
|
+
// no(Object,{entries:r},{entries:function
|
|
157
|
+
if (object) {
|
|
158
|
+
expression += `|\\(${object},\\s*{${property}:.*},\\s*{${property}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
155
161
|
// core-js
|
|
156
162
|
if (object) {
|
|
157
163
|
const objectWithoutPrototype = object.replace('.prototype', '');
|
|
@@ -234,6 +240,9 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
234
240
|
];
|
|
235
241
|
|
|
236
242
|
for (const [name, coreJs2Module] of coreJsPolyfills) {
|
|
243
|
+
// es-shims follows a pattern for its packages.
|
|
244
|
+
// Tack it onto the corejs size estimation, as it is likely close in size.
|
|
245
|
+
const esShimModule = name.toLowerCase();
|
|
237
246
|
data.push({
|
|
238
247
|
name,
|
|
239
248
|
modules: [
|
|
@@ -243,6 +252,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
243
252
|
.replace('es6.', 'es.')
|
|
244
253
|
.replace('es7.', 'es.')
|
|
245
254
|
.replace('typed.', 'typed-array.'),
|
|
255
|
+
esShimModule,
|
|
246
256
|
],
|
|
247
257
|
corejs: true,
|
|
248
258
|
});
|
|
@@ -333,8 +343,9 @@ class LegacyJavascript extends ByteEfficiencyAudit {
|
|
|
333
343
|
// Skip if the pattern matching found a match for this polyfill.
|
|
334
344
|
if (matches.some(m => m.name === name)) continue;
|
|
335
345
|
|
|
336
|
-
const source = bundle.rawMap.sources.find(source =>
|
|
337
|
-
|
|
346
|
+
const source = bundle.rawMap.sources.find(source => modules.some(module => {
|
|
347
|
+
return source.endsWith(`/${module}.js`) || source.includes(`node_modules/${module}/`);
|
|
348
|
+
}));
|
|
338
349
|
if (!source) continue;
|
|
339
350
|
|
|
340
351
|
const mapping = bundle.map.mappings().find(m => m.sourceURL === source);
|
|
@@ -6,15 +6,16 @@ declare class RenderBlockingResources extends Audit {
|
|
|
6
6
|
/**
|
|
7
7
|
* @param {LH.Artifacts} artifacts
|
|
8
8
|
* @param {LH.Audit.Context} context
|
|
9
|
-
* @return {Promise<{
|
|
9
|
+
* @return {Promise<{fcpWastedMs: number, lcpWastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
|
|
10
10
|
*/
|
|
11
11
|
static computeResults(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<{
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
fcpWastedMs: number;
|
|
13
|
+
lcpWastedMs: number;
|
|
14
|
+
results: Array<{
|
|
14
15
|
url: string;
|
|
15
16
|
totalBytes: number;
|
|
16
17
|
wastedMs: number;
|
|
17
|
-
}
|
|
18
|
+
}>;
|
|
18
19
|
}>;
|
|
19
20
|
/**
|
|
20
21
|
* Estimates how much faster this page would reach FCP if we inlined all the used CSS from the
|
|
@@ -17,6 +17,8 @@ import {NetworkRequest} from '../../lib/network-request.js';
|
|
|
17
17
|
import {ProcessedNavigation} from '../../computed/processed-navigation.js';
|
|
18
18
|
import {LoadSimulator} from '../../computed/load-simulator.js';
|
|
19
19
|
import {FirstContentfulPaint} from '../../computed/metrics/first-contentful-paint.js';
|
|
20
|
+
import {LCPImageRecord} from '../../computed/lcp-image-record.js';
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
/** @typedef {import('../../lib/dependency-graph/simulator/simulator').Simulator} Simulator */
|
|
22
24
|
/** @typedef {import('../../lib/dependency-graph/base-node.js').Node} Node */
|
|
@@ -125,7 +127,7 @@ class RenderBlockingResources extends Audit {
|
|
|
125
127
|
/**
|
|
126
128
|
* @param {LH.Artifacts} artifacts
|
|
127
129
|
* @param {LH.Audit.Context} context
|
|
128
|
-
* @return {Promise<{
|
|
130
|
+
* @return {Promise<{fcpWastedMs: number, lcpWastedMs: number, results: Array<{url: string, totalBytes: number, wastedMs: number}>}>}
|
|
129
131
|
*/
|
|
130
132
|
static async computeResults(artifacts, context) {
|
|
131
133
|
const gatherContext = artifacts.GatherContext;
|
|
@@ -179,10 +181,10 @@ class RenderBlockingResources extends Audit {
|
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
if (!results.length) {
|
|
182
|
-
return {results,
|
|
184
|
+
return {results, fcpWastedMs: 0, lcpWastedMs: 0};
|
|
183
185
|
}
|
|
184
186
|
|
|
185
|
-
const
|
|
187
|
+
const fcpWastedMs = RenderBlockingResources.estimateSavingsWithGraphs(
|
|
186
188
|
simulator,
|
|
187
189
|
fcpSimulation.optimisticGraph,
|
|
188
190
|
deferredNodeIds,
|
|
@@ -190,7 +192,10 @@ class RenderBlockingResources extends Audit {
|
|
|
190
192
|
artifacts.Stacks
|
|
191
193
|
);
|
|
192
194
|
|
|
193
|
-
|
|
195
|
+
const lcpRecord = await LCPImageRecord.request(metricComputationData, context);
|
|
196
|
+
|
|
197
|
+
// In most cases if the LCP is an image, render blocking resources don't affect LCP. For these cases we should reduce its impact.
|
|
198
|
+
return {results, fcpWastedMs, lcpWastedMs: lcpRecord ? 0 : fcpWastedMs};
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
/**
|
|
@@ -276,11 +281,12 @@ class RenderBlockingResources extends Audit {
|
|
|
276
281
|
* @return {Promise<LH.Audit.Product>}
|
|
277
282
|
*/
|
|
278
283
|
static async audit(artifacts, context) {
|
|
279
|
-
const {results,
|
|
284
|
+
const {results, fcpWastedMs, lcpWastedMs} =
|
|
285
|
+
await RenderBlockingResources.computeResults(artifacts, context);
|
|
280
286
|
|
|
281
287
|
let displayValue;
|
|
282
288
|
if (results.length > 0) {
|
|
283
|
-
displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs});
|
|
289
|
+
displayValue = str_(i18n.UIStrings.displayValueMsSavings, {wastedMs: fcpWastedMs});
|
|
284
290
|
}
|
|
285
291
|
|
|
286
292
|
/** @type {LH.Audit.Details.Opportunity['headings']} */
|
|
@@ -291,15 +297,15 @@ class RenderBlockingResources extends Audit {
|
|
|
291
297
|
];
|
|
292
298
|
|
|
293
299
|
const details = Audit.makeOpportunityDetails(headings, results,
|
|
294
|
-
{overallSavingsMs:
|
|
300
|
+
{overallSavingsMs: fcpWastedMs});
|
|
295
301
|
|
|
296
302
|
return {
|
|
297
303
|
displayValue,
|
|
298
304
|
score: results.length ? 0 : 1,
|
|
299
|
-
numericValue:
|
|
305
|
+
numericValue: fcpWastedMs,
|
|
300
306
|
numericUnit: 'millisecond',
|
|
301
307
|
details,
|
|
302
|
-
metricSavings: {FCP:
|
|
308
|
+
metricSavings: {FCP: fcpWastedMs, LCP: lcpWastedMs},
|
|
303
309
|
};
|
|
304
310
|
}
|
|
305
311
|
}
|
|
@@ -33,7 +33,7 @@ class UnusedCSSRules extends ByteEfficiencyAudit {
|
|
|
33
33
|
title: str_(UIStrings.title),
|
|
34
34
|
description: str_(UIStrings.description),
|
|
35
35
|
scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.METRIC_SAVINGS,
|
|
36
|
-
guidanceLevel:
|
|
36
|
+
guidanceLevel: 1,
|
|
37
37
|
requiredArtifacts: ['CSSUsage', 'URL', 'devtoolsLogs', 'traces', 'GatherContext'],
|
|
38
38
|
};
|
|
39
39
|
}
|
|
@@ -67,7 +67,7 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
|
|
|
67
67
|
title: str_(UIStrings.title),
|
|
68
68
|
description: str_(UIStrings.description),
|
|
69
69
|
scoreDisplayMode: ByteEfficiencyAudit.SCORING_MODES.METRIC_SAVINGS,
|
|
70
|
-
guidanceLevel:
|
|
70
|
+
guidanceLevel: 1,
|
|
71
71
|
requiredArtifacts: ['JsUsage', 'Scripts', 'SourceMaps', 'GatherContext',
|
|
72
72
|
'devtoolsLogs', 'traces', 'URL'],
|
|
73
73
|
};
|
|
@@ -45,7 +45,7 @@ class LayoutShiftElements extends Audit {
|
|
|
45
45
|
|
|
46
46
|
/** @type {Array<{node: LH.Audit.Details.ItemValue, score: number}>} */
|
|
47
47
|
const clsElementData = artifacts.TraceElements
|
|
48
|
-
.filter(element => element.traceEventType === 'layout-shift')
|
|
48
|
+
.filter(element => element.traceEventType === 'layout-shift-element')
|
|
49
49
|
.map(element => ({
|
|
50
50
|
node: Audit.makeNodeItem(element.node),
|
|
51
51
|
score: impactByNodeId.get(element.nodeId) || 0,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export default LayoutShifts;
|
|
2
|
+
export type Item = LH.Audit.Details.TableItem & {
|
|
3
|
+
node?: LH.Audit.Details.NodeValue;
|
|
4
|
+
score: number;
|
|
5
|
+
subItems?: {
|
|
6
|
+
type: 'subitems';
|
|
7
|
+
items: SubItem[];
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
export type SubItem = {
|
|
11
|
+
extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue;
|
|
12
|
+
cause: LH.IcuMessage;
|
|
13
|
+
};
|
|
14
|
+
declare class LayoutShifts extends Audit {
|
|
15
|
+
/**
|
|
16
|
+
* @param {LH.Artifacts} artifacts
|
|
17
|
+
* @param {LH.Audit.Context} context
|
|
18
|
+
* @return {Promise<LH.Audit.Product>}
|
|
19
|
+
*/
|
|
20
|
+
static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
|
|
21
|
+
}
|
|
22
|
+
export namespace UIStrings {
|
|
23
|
+
const title: string;
|
|
24
|
+
const description: string;
|
|
25
|
+
const columnScore: string;
|
|
26
|
+
const rootCauseUnsizedMedia: string;
|
|
27
|
+
const rootCauseFontChanges: string;
|
|
28
|
+
const rootCauseInjectedIframe: string;
|
|
29
|
+
const rootCauseRenderBlockingRequest: string;
|
|
30
|
+
const displayValueShiftsFound: string;
|
|
31
|
+
}
|
|
32
|
+
import { Audit } from './audit.js';
|
|
33
|
+
//# sourceMappingURL=layout-shifts.d.ts.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license Copyright 2023 Google LLC
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {Audit} from './audit.js';
|
|
7
|
+
import * as i18n from '../lib/i18n/i18n.js';
|
|
8
|
+
import {CumulativeLayoutShift as CumulativeLayoutShiftComputed} from '../computed/metrics/cumulative-layout-shift.js';
|
|
9
|
+
import CumulativeLayoutShift from './metrics/cumulative-layout-shift.js';
|
|
10
|
+
import TraceElements from '../gather/gatherers/trace-elements.js';
|
|
11
|
+
import {TraceEngineResult} from '../computed/trace-engine-result.js';
|
|
12
|
+
|
|
13
|
+
const MAX_LAYOUT_SHIFTS = 15;
|
|
14
|
+
|
|
15
|
+
/** @typedef {LH.Audit.Details.TableItem & {node?: LH.Audit.Details.NodeValue, score: number, subItems?: {type: 'subitems', items: SubItem[]}}} Item */
|
|
16
|
+
/** @typedef {{extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue, cause: LH.IcuMessage}} SubItem */
|
|
17
|
+
|
|
18
|
+
/* eslint-disable max-len */
|
|
19
|
+
const UIStrings = {
|
|
20
|
+
/** Descriptive title of a diagnostic audit that provides the top elements affected by layout shifts. */
|
|
21
|
+
title: 'Avoid large layout shifts',
|
|
22
|
+
/** Description of a diagnostic audit that provides the top elements affected by layout shifts. "windowing" means the metric value is calculated using the subset of events in a small window of time during the run. "normalization" is a good substitute for "windowing". The last sentence starting with 'Learn' becomes link text to additional documentation. */
|
|
23
|
+
description: 'These are the largest layout shifts observed on the page. Each table item represents a single layout shift, and shows the element that shifted the most. Below each item are possible root causes that led to the layout shift. Some of these layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)',
|
|
24
|
+
/** Label for a column in a data table; entries in this column will be a number representing how large the layout shift was. */
|
|
25
|
+
columnScore: 'Layout shift score',
|
|
26
|
+
/** A possible reason why that the layout shift occured. */
|
|
27
|
+
rootCauseUnsizedMedia: 'Media element lacking an explicit size',
|
|
28
|
+
/** A possible reason why that the layout shift occured. */
|
|
29
|
+
rootCauseFontChanges: 'Web font loaded',
|
|
30
|
+
/** A possible reason why that the layout shift occured. */
|
|
31
|
+
rootCauseInjectedIframe: 'Injected iframe',
|
|
32
|
+
/** A possible reason why that the layout shift occured. */
|
|
33
|
+
rootCauseRenderBlockingRequest: 'A late network request adjusted the page layout',
|
|
34
|
+
/** Label shown per-audit to show how many layout shifts are present. The `{# shifts found}` placeholder will be replaced with the number of layout shifts. */
|
|
35
|
+
displayValueShiftsFound: `{shiftCount, plural, =1 {1 layout shift found} other {# layout shifts found}}`,
|
|
36
|
+
};
|
|
37
|
+
/* eslint-enable max-len */
|
|
38
|
+
|
|
39
|
+
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
|
|
40
|
+
|
|
41
|
+
class LayoutShifts extends Audit {
|
|
42
|
+
/**
|
|
43
|
+
* @return {LH.Audit.Meta}
|
|
44
|
+
*/
|
|
45
|
+
static get meta() {
|
|
46
|
+
return {
|
|
47
|
+
id: 'layout-shifts',
|
|
48
|
+
title: str_(UIStrings.title),
|
|
49
|
+
description: str_(UIStrings.description),
|
|
50
|
+
scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
|
|
51
|
+
guidanceLevel: 2,
|
|
52
|
+
requiredArtifacts: ['traces', 'RootCauses', 'TraceElements'],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {LH.Artifacts} artifacts
|
|
58
|
+
* @param {LH.Audit.Context} context
|
|
59
|
+
* @return {Promise<LH.Audit.Product>}
|
|
60
|
+
*/
|
|
61
|
+
static async audit(artifacts, context) {
|
|
62
|
+
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
63
|
+
const traceEngineResult = await TraceEngineResult.request({trace}, context);
|
|
64
|
+
const clusters = traceEngineResult.LayoutShifts.clusters ?? [];
|
|
65
|
+
const {cumulativeLayoutShift: clsSavings, impactByNodeId} =
|
|
66
|
+
await CumulativeLayoutShiftComputed.request(trace, context);
|
|
67
|
+
const traceElements = artifacts.TraceElements
|
|
68
|
+
.filter(element => element.traceEventType === 'layout-shift');
|
|
69
|
+
|
|
70
|
+
/** @type {Item[]} */
|
|
71
|
+
const items = [];
|
|
72
|
+
const layoutShiftEvents = clusters.flatMap(c => c.events);
|
|
73
|
+
const topLayoutShiftEvents = layoutShiftEvents
|
|
74
|
+
.sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta)
|
|
75
|
+
.slice(0, MAX_LAYOUT_SHIFTS);
|
|
76
|
+
for (const event of topLayoutShiftEvents) {
|
|
77
|
+
const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
|
|
78
|
+
event.args.data.impacted_nodes, impactByNodeId);
|
|
79
|
+
const biggestImpactElement = traceElements.find(t => t.nodeId === biggestImpactNodeId);
|
|
80
|
+
|
|
81
|
+
// Turn root causes into sub-items.
|
|
82
|
+
const index = layoutShiftEvents.indexOf(event);
|
|
83
|
+
const rootCauses = artifacts.RootCauses.layoutShifts[index];
|
|
84
|
+
/** @type {SubItem[]} */
|
|
85
|
+
const subItems = [];
|
|
86
|
+
if (rootCauses) {
|
|
87
|
+
for (const cause of rootCauses.unsizedMedia) {
|
|
88
|
+
const element = artifacts.TraceElements.find(
|
|
89
|
+
t => t.traceEventType === 'layout-shift' && t.nodeId === cause.node.backendNodeId);
|
|
90
|
+
subItems.push({
|
|
91
|
+
extra: element ? Audit.makeNodeItem(element.node) : undefined,
|
|
92
|
+
cause: str_(UIStrings.rootCauseUnsizedMedia),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
for (const cause of rootCauses.fontChanges) {
|
|
96
|
+
const url = cause.request.args.data.url;
|
|
97
|
+
subItems.push({
|
|
98
|
+
extra: {type: 'url', value: url},
|
|
99
|
+
cause: str_(UIStrings.rootCauseFontChanges),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
for (const cause of rootCauses.iframes) {
|
|
103
|
+
const element = artifacts.TraceElements.find(
|
|
104
|
+
t => t.traceEventType === 'layout-shift' && t.nodeId === cause.iframe.backendNodeId);
|
|
105
|
+
subItems.push({
|
|
106
|
+
extra: element ? Audit.makeNodeItem(element.node) : undefined,
|
|
107
|
+
cause: str_(UIStrings.rootCauseInjectedIframe),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const cause of rootCauses.renderBlockingRequests) {
|
|
111
|
+
const url = cause.request.args.data.url;
|
|
112
|
+
subItems.push({
|
|
113
|
+
extra: {type: 'url', value: url},
|
|
114
|
+
cause: str_(UIStrings.rootCauseRenderBlockingRequest),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
items.push({
|
|
120
|
+
node: biggestImpactElement ? Audit.makeNodeItem(biggestImpactElement.node) : undefined,
|
|
121
|
+
score: event.args.data.weighted_score_delta,
|
|
122
|
+
subItems: subItems.length ? {type: 'subitems', items: subItems} : undefined,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** @type {LH.Audit.Details.Table['headings']} */
|
|
127
|
+
const headings = [
|
|
128
|
+
/* eslint-disable max-len */
|
|
129
|
+
{key: 'node', valueType: 'node', subItemsHeading: {key: 'extra'}, label: str_(i18n.UIStrings.columnElement)},
|
|
130
|
+
{key: 'score', valueType: 'numeric', subItemsHeading: {key: 'cause', valueType: 'text'}, granularity: 0.001, label: str_(UIStrings.columnScore)},
|
|
131
|
+
/* eslint-enable max-len */
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
const details = Audit.makeTableDetails(headings, items);
|
|
135
|
+
|
|
136
|
+
let displayValue;
|
|
137
|
+
if (items.length > 0) {
|
|
138
|
+
displayValue = str_(UIStrings.displayValueShiftsFound,
|
|
139
|
+
{shiftCount: items.length});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const passed = clsSavings <= CumulativeLayoutShift.defaultOptions.p10;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
score: passed ? 1 : 0,
|
|
146
|
+
scoreDisplayMode: passed ? Audit.SCORING_MODES.INFORMATIVE : undefined,
|
|
147
|
+
metricSavings: {
|
|
148
|
+
CLS: clsSavings,
|
|
149
|
+
},
|
|
150
|
+
notApplicable: details.items.length === 0,
|
|
151
|
+
displayValue,
|
|
152
|
+
details,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export default LayoutShifts;
|
|
158
|
+
export {UIStrings};
|
|
@@ -37,7 +37,7 @@ class PrioritizeLcpImage extends Audit {
|
|
|
37
37
|
title: str_(UIStrings.title),
|
|
38
38
|
description: str_(UIStrings.description),
|
|
39
39
|
supportedModes: ['navigation'],
|
|
40
|
-
guidanceLevel:
|
|
40
|
+
guidanceLevel: 4,
|
|
41
41
|
requiredArtifacts: ['traces', 'devtoolsLogs', 'GatherContext', 'URL', 'TraceElements'],
|
|
42
42
|
scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
|
|
43
43
|
};
|
|
@@ -34,7 +34,7 @@ class UnsizedImages extends Audit {
|
|
|
34
34
|
title: str_(UIStrings.title),
|
|
35
35
|
failureTitle: str_(UIStrings.failureTitle),
|
|
36
36
|
description: str_(UIStrings.description),
|
|
37
|
-
guidanceLevel:
|
|
37
|
+
guidanceLevel: 4,
|
|
38
38
|
requiredArtifacts: ['ImageElements'],
|
|
39
39
|
scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS,
|
|
40
40
|
};
|
package/core/audits/viewport.js
CHANGED
|
@@ -63,12 +63,22 @@ class Viewport extends Audit {
|
|
|
63
63
|
inpSavings = 0;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** @type {LH.Audit.Details.DebugData|undefined} */
|
|
67
|
+
let details;
|
|
68
|
+
if (viewportMeta.rawContentString !== undefined) {
|
|
69
|
+
details = {
|
|
70
|
+
type: 'debugdata',
|
|
71
|
+
viewportContent: viewportMeta.rawContentString,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
return {
|
|
67
76
|
score: Number(viewportMeta.isMobileOptimized),
|
|
68
77
|
metricSavings: {
|
|
69
78
|
INP: inpSavings,
|
|
70
79
|
},
|
|
71
80
|
warnings: viewportMeta.parserWarnings,
|
|
81
|
+
details,
|
|
72
82
|
};
|
|
73
83
|
}
|
|
74
84
|
}
|
|
@@ -4,6 +4,7 @@ export type LayoutShiftEvent = {
|
|
|
4
4
|
isMainFrame: boolean;
|
|
5
5
|
weightedScore: number;
|
|
6
6
|
impactedNodes?: LH.Artifacts.TraceImpactedNode[];
|
|
7
|
+
event: LH.TraceEvent;
|
|
7
8
|
};
|
|
8
9
|
declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
|
|
9
10
|
request: (dependencies: import("../../index.js").Trace, context: import("../../../types/utility-types.js").default.ImmutableObject<{
|
|
@@ -12,6 +13,11 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
|
|
|
12
13
|
cumulativeLayoutShift: number;
|
|
13
14
|
cumulativeLayoutShiftMainFrame: number;
|
|
14
15
|
impactByNodeId: Map<number, number>;
|
|
16
|
+
newEngineResult?: {
|
|
17
|
+
cumulativeLayoutShift: number;
|
|
18
|
+
cumulativeLayoutShiftMainFrame: number;
|
|
19
|
+
} | undefined;
|
|
20
|
+
newEngineResultDiffered: boolean;
|
|
15
21
|
}>;
|
|
16
22
|
};
|
|
17
23
|
declare class CumulativeLayoutShift {
|
|
@@ -43,15 +49,28 @@ declare class CumulativeLayoutShift {
|
|
|
43
49
|
* @return {number}
|
|
44
50
|
*/
|
|
45
51
|
static calculate(layoutShiftEvents: Array<LayoutShiftEvent>): number;
|
|
52
|
+
/**
|
|
53
|
+
* @param {LayoutShiftEvent[]} allFrameShiftEvents
|
|
54
|
+
* @param {LayoutShiftEvent[]} mainFrameShiftEvents
|
|
55
|
+
*/
|
|
56
|
+
static computeWithSharedTraceEngine(allFrameShiftEvents: LayoutShiftEvent[], mainFrameShiftEvents: LayoutShiftEvent[]): Promise<{
|
|
57
|
+
cumulativeLayoutShift: number;
|
|
58
|
+
cumulativeLayoutShiftMainFrame: number;
|
|
59
|
+
}>;
|
|
46
60
|
/**
|
|
47
61
|
* @param {LH.Trace} trace
|
|
48
62
|
* @param {LH.Artifacts.ComputedContext} context
|
|
49
|
-
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number
|
|
63
|
+
* @return {Promise<{cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number, impactByNodeId: Map<number, number>, newEngineResult?: {cumulativeLayoutShift: number, cumulativeLayoutShiftMainFrame: number}, newEngineResultDiffered: boolean}>}
|
|
50
64
|
*/
|
|
51
65
|
static compute_(trace: LH.Trace, context: LH.Artifacts.ComputedContext): Promise<{
|
|
52
66
|
cumulativeLayoutShift: number;
|
|
53
67
|
cumulativeLayoutShiftMainFrame: number;
|
|
54
68
|
impactByNodeId: Map<number, number>;
|
|
69
|
+
newEngineResult?: {
|
|
70
|
+
cumulativeLayoutShift: number;
|
|
71
|
+
cumulativeLayoutShiftMainFrame: number;
|
|
72
|
+
} | undefined;
|
|
73
|
+
newEngineResultDiffered: boolean;
|
|
55
74
|
}>;
|
|
56
75
|
}
|
|
57
76
|
import { ProcessedTrace } from '../processed-trace.js';
|