webperf-snippets 1.3.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 +67 -6
- package/package.json +3 -2
- package/snippets/CoreWebVitals/CLS.js +3 -2
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +8 -9
- package/snippets/CoreWebVitals/LCP-Subparts.js +32 -35
- 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 +225 -0
- package/src/bin.js +68 -7
- package/src/blocking.js +3 -1
- package/src/conditions.js +7 -0
- package/src/mcp/documents.js +41 -21
- package/src/reporters/baseline-view.js +83 -0
- package/src/reporters/human.js +43 -5
- package/src/reporters/markdown.js +46 -6
- package/src/reporters/matrix-view.js +8 -2
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
count: 0,
|
|
23
23
|
details: {},
|
|
24
24
|
items: [],
|
|
25
|
-
issues: [{ severity: "error", message: "Service Workers not supported in this browser" }],
|
|
25
|
+
issues: [{ code: "unsupported", severity: "error", message: "Service Workers not supported in this browser" }],
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -116,11 +116,12 @@
|
|
|
116
116
|
const issues = [];
|
|
117
117
|
|
|
118
118
|
if (registrations.length === 0) {
|
|
119
|
-
issues.push({ severity: "info", message: "No Service Workers registered" });
|
|
119
|
+
issues.push({ code: "no-service-worker", severity: "info", message: "No Service Workers registered" });
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
if (registrations.length > 0 && !controller) {
|
|
123
123
|
issues.push({
|
|
124
|
+
code: "not-controlled",
|
|
124
125
|
severity: "warning",
|
|
125
126
|
message: "Page is not controlled by a Service Worker (hard reload or first visit). Do a normal reload.",
|
|
126
127
|
});
|
|
@@ -129,25 +130,31 @@
|
|
|
129
130
|
for (const item of items) {
|
|
130
131
|
if (item.hasWaiting) {
|
|
131
132
|
issues.push({
|
|
133
|
+
code: "update-pending",
|
|
132
134
|
severity: "warning",
|
|
133
135
|
message: `Service Worker update pending for ${item.scope}. Call skipWaiting() to activate the new version.`,
|
|
136
|
+
target: item.scope,
|
|
134
137
|
});
|
|
135
138
|
}
|
|
136
139
|
if (item.navigationPreloadEnabled === false) {
|
|
137
140
|
issues.push({
|
|
141
|
+
code: "navigation-preload-disabled",
|
|
138
142
|
severity: "info",
|
|
139
143
|
message: `Navigation Preload is disabled for ${item.scope}. Consider enabling it with registration.navigationPreload.enable().`,
|
|
144
|
+
target: item.scope,
|
|
140
145
|
});
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
if (swOverheadMs !== null && swOverheadMs > 100) {
|
|
145
150
|
issues.push({
|
|
151
|
+
code: "slow-startup",
|
|
146
152
|
severity: "error",
|
|
147
153
|
message: `High SW startup time (${swOverheadMs}ms). Enable Navigation Preload to reduce overhead.`,
|
|
148
154
|
});
|
|
149
155
|
} else if (swOverheadMs !== null && swOverheadMs > 50) {
|
|
150
156
|
issues.push({
|
|
157
|
+
code: "slow-startup",
|
|
151
158
|
severity: "warning",
|
|
152
159
|
message: `Moderate SW startup time (${swOverheadMs}ms)`,
|
|
153
160
|
});
|
|
@@ -155,11 +162,13 @@
|
|
|
155
162
|
|
|
156
163
|
if (cacheHitRate !== null && cacheHitRate < 50) {
|
|
157
164
|
issues.push({
|
|
165
|
+
code: "low-cache-hit-rate",
|
|
158
166
|
severity: "error",
|
|
159
167
|
message: `Low SW cache hit rate (${cacheHitRate}%). Review the caching strategy.`,
|
|
160
168
|
});
|
|
161
169
|
} else if (cacheHitRate !== null && cacheHitRate < 80) {
|
|
162
170
|
issues.push({
|
|
171
|
+
code: "low-cache-hit-rate",
|
|
163
172
|
severity: "warning",
|
|
164
173
|
message: `Moderate SW cache hit rate (${cacheHitRate}%). Review the caching strategy for more resources.`,
|
|
165
174
|
});
|
|
@@ -167,6 +176,7 @@
|
|
|
167
176
|
|
|
168
177
|
if (unknownSource.length > 0) {
|
|
169
178
|
issues.push({
|
|
179
|
+
code: "unknown-source",
|
|
170
180
|
severity: "info",
|
|
171
181
|
message: `${unknownSource.length} intercepted resource(s) are cross-origin without Timing-Allow-Origin, so their source is unknown. The hit rate excludes them.`,
|
|
172
182
|
});
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
parsed = JSON.parse(script.textContent);
|
|
40
40
|
} catch (e) {
|
|
41
41
|
issues.push({
|
|
42
|
+
code: 'invalid-json',
|
|
42
43
|
severity: 'error',
|
|
43
44
|
message: `Speculation rules script #${index + 1} is not valid JSON (${e.message}). The browser ignores the whole script.`,
|
|
44
45
|
});
|
|
@@ -68,12 +69,14 @@
|
|
|
68
69
|
const isPrerender = action === 'prerender' || action === 'prerender_until_script';
|
|
69
70
|
if (isPrerender && source === 'list' && (eagerness === 'immediate' || eagerness === 'eager') && urls.length > EAGER_PRERENDER_LIMIT) {
|
|
70
71
|
issues.push({
|
|
72
|
+
code: 'eager-list-capped',
|
|
71
73
|
severity: 'warning',
|
|
72
74
|
message: `A ${action} rule lists ${urls.length} URLs with eagerness "${eagerness}". Chrome caps eager prerenders, so the extra URLs are not prerendered. Fix: list fewer URLs or use "moderate".`,
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
if (isPrerender && source === 'document' && (eagerness === 'immediate' || eagerness === 'eager')) {
|
|
76
78
|
issues.push({
|
|
79
|
+
code: 'eager-document-rule',
|
|
77
80
|
severity: 'warning',
|
|
78
81
|
message: `A document ${action} rule uses eagerness "${eagerness}", which prerenders matching links without a user signal and can waste bandwidth and CPU. Fix: use "moderate" or "conservative".`,
|
|
79
82
|
});
|
|
@@ -83,15 +86,15 @@
|
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
if (scripts.length === 0) {
|
|
86
|
-
issues.push({ severity: 'info', message: 'No speculation rules found in the DOM. Rules delivered in the Speculation-Rules HTTP header are not readable from JavaScript.' });
|
|
89
|
+
issues.push({ code: 'no-rules', severity: 'info', message: 'No speculation rules found in the DOM. Rules delivered in the Speculation-Rules HTTP header are not readable from JavaScript.' });
|
|
87
90
|
} else if (items.length === 0 && !issues.some((i) => i.severity === 'error')) {
|
|
88
|
-
issues.push({ severity: 'info', message: 'Speculation rules scripts were found but contain no prefetch or prerender rules.' });
|
|
91
|
+
issues.push({ code: 'no-prefetch-or-prerender', severity: 'info', message: 'Speculation rules scripts were found but contain no prefetch or prerender rules.' });
|
|
89
92
|
}
|
|
90
93
|
if (!supported && scripts.length > 0) {
|
|
91
|
-
issues.push({ severity: 'info', message: 'This browser does not support speculation rules, so the rules are ignored.' });
|
|
94
|
+
issues.push({ code: 'unsupported', severity: 'info', message: 'This browser does not support speculation rules, so the rules are ignored.' });
|
|
92
95
|
}
|
|
93
96
|
if (activationStart > 0) {
|
|
94
|
-
issues.push({ severity: 'info', message: `This page was prerendered and activated at ${activationStart}ms. Navigation timings are relative to activationStart.` });
|
|
97
|
+
issues.push({ code: 'prerendered', severity: 'info', message: `This page was prerendered and activated at ${activationStart}ms. Navigation timings are relative to activationStart.` });
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
return {
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
corsLimitedAnalysis: corsRestrictedCount > 0,
|
|
169
169
|
details: { avgTtfbMs: null, maxTtfbMs: null, minTtfbMs: null, thirdPartyCount: 0, slowCount: 0, corsRestrictedCount },
|
|
170
170
|
items: [],
|
|
171
|
-
issues: [{ severity: "info", message: "No resources with TTFB data available" }],
|
|
171
|
+
issues: [{ code: "no-resources", severity: "info", message: "No resources with TTFB data available" }],
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
const ttfbVals = resourcesSync.map((r) => r.ttfbMs);
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
const slowestMs = Math.round(slowestSync.duration);
|
|
130
130
|
const share = ttfbValueSync > 0 ? Math.round((slowestSync.duration / ttfbValueSync) * 100) : 0;
|
|
131
131
|
issues.push({
|
|
132
|
+
code: "slow-ttfb",
|
|
132
133
|
severity: rating === "poor" ? "error" : "warning",
|
|
133
134
|
message: `TTFB is ${value} ms (${rating}); ${phaseLabels[slowestSync.key]} takes ${slowestMs} ms of it (${share}%). Fix: ${phaseFixes[slowestSync.key]}.`,
|
|
134
135
|
});
|
|
@@ -187,30 +187,36 @@
|
|
|
187
187
|
const issues = [];
|
|
188
188
|
if (blocking.length > 0) {
|
|
189
189
|
issues.push({
|
|
190
|
+
code: "render-blocking-domains",
|
|
190
191
|
severity: "warning",
|
|
191
192
|
message: `${blocking.length} third-party domain(s) serve render-blocking resources (${listDomains(blocking)}); load them async or defer, or self-host them`,
|
|
192
193
|
});
|
|
193
194
|
}
|
|
194
195
|
heavy.slice(0, MAX_LISTED_DOMAINS).forEach((d) => {
|
|
195
196
|
issues.push({
|
|
197
|
+
code: "heavy-domain",
|
|
196
198
|
severity: "warning",
|
|
197
199
|
message: `${d.domain} scripts take ${d.loafMs} ms of main-thread time in long animation frames (over ${LOAF_WARNING_MS} ms); defer, delay or remove them`,
|
|
200
|
+
target: d.domain,
|
|
198
201
|
});
|
|
199
202
|
});
|
|
200
203
|
if (!renderBlockingSupported && all.length > 0) {
|
|
201
204
|
issues.push({
|
|
205
|
+
code: "render-blocking-unsupported",
|
|
202
206
|
severity: "info",
|
|
203
207
|
message: "renderBlockingStatus is not supported in this browser (Chrome 107+), so which third-party domains block rendering is unknown; run the snippet in Chrome or Edge to check",
|
|
204
208
|
});
|
|
205
209
|
}
|
|
206
210
|
if (loafUnattributed) {
|
|
207
211
|
issues.push({
|
|
212
|
+
code: "loaf-unattributed",
|
|
208
213
|
severity: "info",
|
|
209
214
|
message: `${longFrames} long animation frame(s) took ${longFramesMs} ms, but no third-party script was attributed in them (Long Animation Frames only list scripts over 5 ms), so 0 ms per domain does not rule out third-party cost`,
|
|
210
215
|
});
|
|
211
216
|
}
|
|
212
217
|
if (corsLimitedAnalysis) {
|
|
213
218
|
issues.push({
|
|
219
|
+
code: "unknown-size",
|
|
214
220
|
severity: "info",
|
|
215
221
|
message: `${sizeUnknownCount} third-party request(s) without Timing-Allow-Origin report zero sizes, so the transfer size is a lower bound`,
|
|
216
222
|
});
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
} else {
|
|
84
84
|
// Preload without matching script and not loaded - likely unused
|
|
85
85
|
issues.push({
|
|
86
|
+
code: "orphan-preload",
|
|
86
87
|
type: "orphan",
|
|
87
88
|
severity: "warning",
|
|
88
89
|
preload,
|
|
@@ -137,6 +138,7 @@
|
|
|
137
138
|
});
|
|
138
139
|
} else {
|
|
139
140
|
issues.push({
|
|
141
|
+
code: "async-defer-preload",
|
|
140
142
|
type: "async-defer-preload",
|
|
141
143
|
severity: "error",
|
|
142
144
|
preload,
|
|
@@ -156,6 +158,7 @@
|
|
|
156
158
|
} else if (isModule) {
|
|
157
159
|
// ES modules without async/defer behave like defer by default
|
|
158
160
|
issues.push({
|
|
161
|
+
code: "module-preload",
|
|
159
162
|
type: "module-preload",
|
|
160
163
|
severity: "warning",
|
|
161
164
|
preload,
|
|
@@ -413,8 +416,10 @@
|
|
|
413
416
|
},
|
|
414
417
|
items: Array.from(itemsByUrl.values()).slice(0, 50),
|
|
415
418
|
issues: issues.map(i => ({
|
|
419
|
+
code: i.code,
|
|
416
420
|
severity: i.severity,
|
|
417
421
|
message: i.fix ? `${i.message.replace(/\.$/, "")}. Fix: ${i.fix}` : i.message,
|
|
422
|
+
target: i.url,
|
|
418
423
|
})),
|
|
419
424
|
};
|
|
420
425
|
})();
|
|
@@ -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
|
};
|