webperf-snippets 1.4.0 → 1.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/README.md +5 -5
- package/package.json +1 -1
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +8 -9
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +14 -13
- package/snippets/Interaction/DOM-Size-and-Depth.js +4 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +2 -0
- package/snippets/Interaction/Input-Latency-Breakdown.js +2 -0
- package/snippets/Interaction/Interactions.js +2 -0
- package/snippets/Interaction/Scroll-Performance.js +2 -0
- package/snippets/Loading/Back-Forward-Cache.js +7 -0
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +4 -2
- package/snippets/Loading/Cache-Strategy-Analysis.js +12 -0
- package/snippets/Loading/Client-Side-Redirect-Detection.js +5 -3
- package/snippets/Loading/Compression-Audit.js +3 -0
- package/snippets/Loading/Content-Visibility.js +2 -2
- package/snippets/Loading/Critical-CSS-Detection.js +6 -0
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +2 -2
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +1 -1
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +1 -1
- package/snippets/Loading/Find-render-blocking-resources.js +1 -1
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +5 -2
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +4 -4
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +3 -3
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +5 -1
- package/snippets/Loading/Inline-Script-Info-and-Size.js +5 -1
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +4 -4
- package/snippets/Loading/Prefetch-Resource-Validation.js +12 -3
- package/snippets/Loading/Priority-Hints-Audit.js +10 -0
- package/snippets/Loading/Resource-Hints-Validation.js +3 -3
- package/snippets/Loading/Resource-Hints.js +7 -1
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +5 -2
- package/snippets/Loading/Script-Loading.js +6 -2
- package/snippets/Loading/Server-Timing-Early-Hints.js +3 -0
- package/snippets/Loading/Service-Worker-Analysis.js +12 -2
- package/snippets/Loading/Speculation-Rules-Inspector.js +7 -4
- package/snippets/Loading/TTFB-Resources.js +1 -1
- package/snippets/Loading/TTFB-Sub-Parts.js +1 -0
- package/snippets/Loading/Third-Party-Impact-by-Domain.js +6 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +5 -0
- package/snippets/Loading/Webfont-Usage-Analyzer.js +13 -14
- package/snippets/Media/Image-Element-Audit.js +16 -13
- package/snippets/Media/Oversized-Images.js +4 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +2 -1
- package/snippets/Media/Video-Element-Audit.js +13 -2
- package/snippets/SCHEMA.md +425 -3
- package/src/baseline.js +30 -6
- package/src/blocking.js +1 -1
- package/src/mcp/documents.js +41 -21
|
@@ -269,43 +269,42 @@
|
|
|
269
269
|
};
|
|
270
270
|
|
|
271
271
|
const issues = [
|
|
272
|
-
...failed.map((f) => ({ severity: "error", message: `Font failed to load: ${label(f)}
|
|
272
|
+
...failed.map((f) => ({ code: "load-failed", severity: "error", message: `Font failed to load: ${label(f)}`, target: f.url || label(f) })),
|
|
273
273
|
...[...new Map(loadedUnused.map((f) => [`${label(f)}|${f.url}`, f])).values()].map((f) => {
|
|
274
274
|
const bytes = wastedBytes(f) ? `, ${kb(wastedBytes(f))} downloaded for nothing` : "";
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
message: f.
|
|
278
|
-
|
|
279
|
-
: `Loaded but not used by any rendered element: ${label(f)}${bytes}`,
|
|
280
|
-
};
|
|
275
|
+
const target = f.url || label(f);
|
|
276
|
+
return f._redeclared
|
|
277
|
+
? { code: "redeclared", severity: "warning", message: `Loaded but declared again later with the same descriptors, so the page renders with the other file: ${label(f)}${f.url ? ` from ${f.url}` : ""}${bytes}`, target }
|
|
278
|
+
: { code: "loaded-unused", severity: "warning", message: `Loaded but not used by any rendered element: ${label(f)}${bytes}`, target };
|
|
281
279
|
}),
|
|
282
280
|
];
|
|
283
281
|
if (blockingDisplay.length > 0) {
|
|
284
282
|
issues.push({
|
|
283
|
+
code: "blocking-font-display",
|
|
285
284
|
severity: "warning",
|
|
286
285
|
message: `font-display is auto or block on ${uniqueLabels(blockingDisplay).length} face(s), so text can stay invisible while they load. Use swap or optional: ${names(blockingDisplay)}`,
|
|
287
286
|
});
|
|
288
287
|
}
|
|
289
288
|
if (totalFontBytes > BYTES_WARNING) {
|
|
290
|
-
issues.push({ severity: "warning", message: `Web fonts weigh ${kb(totalFontBytes)} in total (over ${kb(BYTES_WARNING)}). Subset, use WOFF2 or drop unused weights` });
|
|
289
|
+
issues.push({ code: "heavy-fonts", severity: "warning", message: `Web fonts weigh ${kb(totalFontBytes)} in total (over ${kb(BYTES_WARNING)}). Subset, use WOFF2 or drop unused weights` });
|
|
291
290
|
}
|
|
292
291
|
if (neverLoadedWhole.length > 0) {
|
|
293
|
-
issues.push({ severity: "info", message: `Declared but never loaded (${uniqueLabels(neverLoadedWhole).length}); nothing on this page needed them: ${names(neverLoadedWhole)}` });
|
|
292
|
+
issues.push({ code: "never-loaded", severity: "info", message: `Declared but never loaded (${uniqueLabels(neverLoadedWhole).length}); nothing on this page needed them: ${names(neverLoadedWhole)}` });
|
|
294
293
|
}
|
|
295
294
|
if (shadowed.length > 0) {
|
|
296
|
-
issues.push({ severity: "info", message: `Declared more than once with the same descriptors; the page uses a later declaration, so these never loaded (${uniqueLabels(shadowed).length}): ${names(shadowed)}` });
|
|
295
|
+
issues.push({ code: "shadowed", severity: "info", message: `Declared more than once with the same descriptors; the page uses a later declaration, so these never loaded (${uniqueLabels(shadowed).length}): ${names(shadowed)}` });
|
|
297
296
|
}
|
|
298
297
|
if (neverLoadedSubsets.length > 0) {
|
|
299
|
-
issues.push({ severity: "info", message: `unicode-range subsets this page did not need (${uniqueLabels(neverLoadedSubsets).length}), which is the intended behavior of a split font: ${names(neverLoadedSubsets)}` });
|
|
298
|
+
issues.push({ code: "unused-subsets", severity: "info", message: `unicode-range subsets this page did not need (${uniqueLabels(neverLoadedSubsets).length}), which is the intended behavior of a split font: ${names(neverLoadedSubsets)}` });
|
|
300
299
|
}
|
|
301
300
|
if (!bytesReadable) {
|
|
302
|
-
issues.push({ severity: "info", message: "Some font files are cross-origin without Timing-Allow-Origin, so their size reads as 0 and the byte totals are a lower bound" });
|
|
301
|
+
issues.push({ code: "size-unknown", severity: "info", message: "Some font files are cross-origin without Timing-Allow-Origin, so their size reads as 0 and the byte totals are a lower bound" });
|
|
303
302
|
}
|
|
304
303
|
if (unreadableStylesheets > 0) {
|
|
305
|
-
issues.push({ severity: "info", message: `${unreadableStylesheets} cross-origin stylesheet(s) could not be read, so their font files cannot be matched to faces` });
|
|
304
|
+
issues.push({ code: "unreadable-stylesheets", severity: "info", message: `${unreadableStylesheets} cross-origin stylesheet(s) could not be read, so their font files cannot be matched to faces` });
|
|
306
305
|
}
|
|
307
306
|
if (scanned < elements.length) {
|
|
308
|
-
issues.push({ severity: "info", message: `Only the first ${MAX_ELEMENTS} elements were scanned for text; some faces may be reported as unused` });
|
|
307
|
+
issues.push({ code: "elements-capped", severity: "info", message: `Only the first ${MAX_ELEMENTS} elements were scanned for text; some faces may be reported as unused` });
|
|
309
308
|
}
|
|
310
309
|
|
|
311
310
|
// --- One row per face and file --------------------------------------------------------
|
|
@@ -199,34 +199,34 @@
|
|
|
199
199
|
|
|
200
200
|
if (isLcp) {
|
|
201
201
|
if (fetchpriority !== "high")
|
|
202
|
-
issues.push({ s: "error", msg: 'Add fetchpriority="high" to the LCP image' });
|
|
202
|
+
issues.push({ code: "lcp-no-fetchpriority", s: "error", msg: 'Add fetchpriority="high" to the LCP image' });
|
|
203
203
|
if (loading === "lazy")
|
|
204
|
-
issues.push({ s: "error", msg: 'Remove loading="lazy" from the LCP image' });
|
|
204
|
+
issues.push({ code: "lcp-lazy", s: "error", msg: 'Remove loading="lazy" from the LCP image' });
|
|
205
205
|
if (decoding !== "sync")
|
|
206
|
-
issues.push({ s: "warning", msg: 'Consider decoding="sync" for the LCP image' });
|
|
206
|
+
issues.push({ code: "lcp-not-decoding-sync", s: "warning", msg: 'Consider decoding="sync" for the LCP image' });
|
|
207
207
|
lcpPreload = findPreloadForLcp(img, imagePreloads);
|
|
208
208
|
if (!lcpPreload)
|
|
209
|
-
issues.push({ s: "warning", msg: 'LCP image has no <link rel="preload" as="image">' });
|
|
209
|
+
issues.push({ code: "lcp-not-preloaded", s: "warning", msg: 'LCP image has no <link rel="preload" as="image">' });
|
|
210
210
|
else if (lcpPreload.getAttribute("fetchpriority") !== "high")
|
|
211
|
-
issues.push({ s: "info", msg: 'LCP image preload is missing fetchpriority="high"' });
|
|
211
|
+
issues.push({ code: "lcp-preload-no-fetchpriority", s: "info", msg: 'LCP image preload is missing fetchpriority="high"' });
|
|
212
212
|
} else if (inViewport) {
|
|
213
213
|
if (loading === "lazy")
|
|
214
|
-
issues.push({ s: "warning", msg: 'Remove loading="lazy" (image is above the fold)' });
|
|
214
|
+
issues.push({ code: "lazy-in-viewport", s: "warning", msg: 'Remove loading="lazy" (image is above the fold)' });
|
|
215
215
|
} else {
|
|
216
216
|
if (loading !== "lazy")
|
|
217
|
-
issues.push({ s: "warning", msg: 'Add loading="lazy" (image is off-viewport)' });
|
|
217
|
+
issues.push({ code: "eager-off-viewport", s: "warning", msg: 'Add loading="lazy" (image is off-viewport)' });
|
|
218
218
|
if (fetchpriority === "high")
|
|
219
|
-
issues.push({ s: "error", msg: 'Remove fetchpriority="high" (image is off-viewport)' });
|
|
219
|
+
issues.push({ code: "high-priority-off-viewport", s: "error", msg: 'Remove fetchpriority="high" (image is off-viewport)' });
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
if (loading === "lazy" && fetchpriority === "high")
|
|
223
|
-
issues.push({ s: "error", msg: 'Conflict: loading="lazy" + fetchpriority="high"' });
|
|
223
|
+
issues.push({ code: "lazy-and-high-priority", s: "error", msg: 'Conflict: loading="lazy" + fetchpriority="high"' });
|
|
224
224
|
|
|
225
225
|
if (!hasDimensions)
|
|
226
|
-
issues.push({ s: "warning", msg: "Missing width/height attributes (CLS risk)" });
|
|
226
|
+
issues.push({ code: "missing-dimensions", s: "warning", msg: "Missing width/height attributes (CLS risk)" });
|
|
227
227
|
|
|
228
228
|
if (!isModernFormat(format) && !inPicture && format !== "svg")
|
|
229
|
-
issues.push({ s: "info", msg: "No modern format detected (WebP / AVIF / JXL)" });
|
|
229
|
+
issues.push({ code: "no-modern-format", s: "info", msg: "No modern format detected (WebP / AVIF / JXL)" });
|
|
230
230
|
|
|
231
231
|
return {
|
|
232
232
|
img,
|
|
@@ -413,16 +413,19 @@
|
|
|
413
413
|
hasSrcset: r.hasSrcset,
|
|
414
414
|
hasSizes: r.hasSizes,
|
|
415
415
|
inPicture: r.inPicture,
|
|
416
|
-
issues: r.issues.map((i) => ({ severity: i.s, message: i.msg })),
|
|
416
|
+
issues: r.issues.map((i) => ({ code: i.code, severity: i.s, message: i.msg })),
|
|
417
417
|
})),
|
|
418
418
|
issues: [
|
|
419
419
|
...(nonImageTag
|
|
420
|
-
? [{ severity: "info", message: `The LCP element is not an <img> (${nonImageTag === "unknown" ? "not in the page's images" : nonImageTag}), so no image is audited as the LCP` }]
|
|
420
|
+
? [{ code: "lcp-not-img", severity: "info", message: `The LCP element is not an <img> (${nonImageTag === "unknown" ? "not in the page's images" : nonImageTag}), so no image is audited as the LCP` }]
|
|
421
421
|
: []),
|
|
422
422
|
...audited.flatMap((r) =>
|
|
423
423
|
r.issues.map((i) => ({
|
|
424
|
+
code: i.code,
|
|
424
425
|
severity: i.s,
|
|
425
426
|
message: `${shortSrc(r.src) || "(no src)"}: ${i.msg}`,
|
|
427
|
+
// The URL names the image; a data: URL is cut, as it can weigh more than the result
|
|
428
|
+
...(r.src ? { target: r.src.startsWith("data:") ? r.src.slice(0, 60) : r.src } : {}),
|
|
426
429
|
}))
|
|
427
430
|
),
|
|
428
431
|
],
|
|
@@ -186,13 +186,17 @@
|
|
|
186
186
|
if (f.verdict === "oversized") {
|
|
187
187
|
const kb = f.wastedBytes === null ? "" : `, about ${Math.round(f.wastedBytes / 1024)} KB wasted`;
|
|
188
188
|
return {
|
|
189
|
+
code: "oversized",
|
|
189
190
|
severity: f.ratio >= SEVERE_OVERSIZED ? "warning" : "info",
|
|
190
191
|
message: `${fileName(f.url)}: ${size} file for a ${shown} slot at ${dpr}x (${f.ratio}x too large${kb})`,
|
|
192
|
+
target: f.url,
|
|
191
193
|
};
|
|
192
194
|
}
|
|
193
195
|
return {
|
|
196
|
+
code: "undersized",
|
|
194
197
|
severity: f.ratio < SEVERE_UNDERSIZED ? "warning" : "info",
|
|
195
198
|
message: `${fileName(f.url)}: ${size} file for a ${shown} slot at ${dpr}x (blurry, needs ${round2(1 / f.ratio)}x more pixels)`,
|
|
199
|
+
target: f.url,
|
|
196
200
|
};
|
|
197
201
|
});
|
|
198
202
|
|
|
@@ -242,7 +242,7 @@
|
|
|
242
242
|
|
|
243
243
|
const issues = [];
|
|
244
244
|
if (withBitmaps.length > 0) {
|
|
245
|
-
issues.push({ severity: "warning", message: `${withBitmaps.length} SVG file(s) contain embedded bitmaps` });
|
|
245
|
+
issues.push({ code: "embedded-bitmaps", severity: "warning", message: `${withBitmaps.length} SVG file(s) contain embedded bitmaps` });
|
|
246
246
|
}
|
|
247
247
|
if (spriteOpportunity) {
|
|
248
248
|
const withUse =
|
|
@@ -250,6 +250,7 @@
|
|
|
250
250
|
? ` (${svgsWithUse} inline SVG${svgsWithUse === 1 ? " already uses" : "s already use"} <use>)`
|
|
251
251
|
: "";
|
|
252
252
|
issues.push({
|
|
253
|
+
code: "sprite-opportunity",
|
|
253
254
|
severity: "info",
|
|
254
255
|
message: `${standaloneInlineSvgs} standalone inline SVGs could share one <symbol> sprite referenced with <use>${withUse}`,
|
|
255
256
|
});
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
status: "ok",
|
|
61
61
|
count: 0,
|
|
62
62
|
items: [],
|
|
63
|
-
issues: [{ severity: "info", message: "No <video> elements on this page, so there was nothing to audit." }],
|
|
63
|
+
issues: [{ code: "no-videos", severity: "info", message: "No <video> elements on this page, so there was nothing to audit." }],
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
|
|
85
85
|
if (preload === "auto" && !autoplay) {
|
|
86
86
|
issues.push({
|
|
87
|
+
code: "preload-auto",
|
|
87
88
|
s: "error",
|
|
88
89
|
msg: 'preload="auto" loads the full video eagerly: use preload="none" or preload="metadata"',
|
|
89
90
|
});
|
|
@@ -91,6 +92,7 @@
|
|
|
91
92
|
|
|
92
93
|
if (!autoplay && !inViewport && preload !== "none") {
|
|
93
94
|
issues.push({
|
|
95
|
+
code: "off-viewport-preload",
|
|
94
96
|
s: "warning",
|
|
95
97
|
msg: 'Off-viewport video without preload="none": set preload="none" to avoid unnecessary loading',
|
|
96
98
|
});
|
|
@@ -98,6 +100,7 @@
|
|
|
98
100
|
|
|
99
101
|
if (autoplay && !muted) {
|
|
100
102
|
issues.push({
|
|
103
|
+
code: "autoplay-not-muted",
|
|
101
104
|
s: "error",
|
|
102
105
|
msg: "autoplay without muted: browsers block unmuted autoplay",
|
|
103
106
|
});
|
|
@@ -105,6 +108,7 @@
|
|
|
105
108
|
|
|
106
109
|
if (autoplay && !playsinline) {
|
|
107
110
|
issues.push({
|
|
111
|
+
code: "autoplay-no-playsinline",
|
|
108
112
|
s: "warning",
|
|
109
113
|
msg: "autoplay without playsinline: iOS Safari forces fullscreen mode",
|
|
110
114
|
});
|
|
@@ -112,6 +116,7 @@
|
|
|
112
116
|
|
|
113
117
|
if (!poster) {
|
|
114
118
|
issues.push({
|
|
119
|
+
code: "missing-poster",
|
|
115
120
|
s: "warning",
|
|
116
121
|
msg: "Missing poster: no preview frame shown before playback starts",
|
|
117
122
|
});
|
|
@@ -119,6 +124,7 @@
|
|
|
119
124
|
|
|
120
125
|
if (!hasDimensions) {
|
|
121
126
|
issues.push({
|
|
127
|
+
code: "missing-dimensions",
|
|
122
128
|
s: "warning",
|
|
123
129
|
msg: "Missing width/height attributes (CLS risk)",
|
|
124
130
|
});
|
|
@@ -126,6 +132,7 @@
|
|
|
126
132
|
|
|
127
133
|
if (!isModernCodec(codec) && codec !== "no source") {
|
|
128
134
|
issues.push({
|
|
135
|
+
code: "no-modern-codec",
|
|
129
136
|
s: "info",
|
|
130
137
|
msg: "No modern codec source detected: consider adding a WebM source (VP9 or AV1) for better compression",
|
|
131
138
|
});
|
|
@@ -133,6 +140,7 @@
|
|
|
133
140
|
|
|
134
141
|
if (!controls && !autoplay) {
|
|
135
142
|
issues.push({
|
|
143
|
+
code: "no-controls",
|
|
136
144
|
s: "info",
|
|
137
145
|
msg: "No controls attribute and no autoplay, so users may not be able to play the video",
|
|
138
146
|
});
|
|
@@ -276,12 +284,15 @@
|
|
|
276
284
|
hasPoster: r.poster !== "(not set)",
|
|
277
285
|
hasDimensions: r.hasDimensions,
|
|
278
286
|
sourceCount: r.sourceCount,
|
|
279
|
-
issues: r.issues.map((i) => ({ severity: i.s, message: i.msg })),
|
|
287
|
+
issues: r.issues.map((i) => ({ code: i.code, severity: i.s, message: i.msg })),
|
|
280
288
|
})),
|
|
281
289
|
issues: audited.flatMap((r) =>
|
|
282
290
|
r.issues.map((i) => ({
|
|
291
|
+
code: i.code,
|
|
283
292
|
severity: i.s,
|
|
284
293
|
message: `${shortSrc(r.src) || "(no src)"}: ${i.msg}`,
|
|
294
|
+
// The URL names the video; a data: URL is cut, as it can weigh more than the result
|
|
295
|
+
...(r.src ? { target: r.src.startsWith("data:") ? r.src.slice(0, 60) : r.src } : {}),
|
|
285
296
|
}))
|
|
286
297
|
),
|
|
287
298
|
};
|