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.
Files changed (48) hide show
  1. package/README.md +5 -5
  2. package/package.json +1 -1
  3. package/snippets/CoreWebVitals/LCP-Image-Entropy.js +8 -9
  4. package/snippets/CoreWebVitals/LCP-Video-Candidate.js +14 -13
  5. package/snippets/Interaction/DOM-Size-and-Depth.js +4 -0
  6. package/snippets/Interaction/Forced-Synchronous-Layout.js +2 -0
  7. package/snippets/Interaction/Input-Latency-Breakdown.js +2 -0
  8. package/snippets/Interaction/Interactions.js +2 -0
  9. package/snippets/Interaction/Scroll-Performance.js +2 -0
  10. package/snippets/Loading/Back-Forward-Cache.js +7 -0
  11. package/snippets/Loading/CSS-Media-Queries-Analysis.js +4 -2
  12. package/snippets/Loading/Cache-Strategy-Analysis.js +12 -0
  13. package/snippets/Loading/Client-Side-Redirect-Detection.js +5 -3
  14. package/snippets/Loading/Compression-Audit.js +3 -0
  15. package/snippets/Loading/Content-Visibility.js +2 -2
  16. package/snippets/Loading/Critical-CSS-Detection.js +6 -0
  17. package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +2 -2
  18. package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +1 -1
  19. package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +1 -1
  20. package/snippets/Loading/Find-render-blocking-resources.js +1 -1
  21. package/snippets/Loading/First-And-Third-Party-Script-Info.js +5 -2
  22. package/snippets/Loading/First-And-Third-Party-Script-Timings.js +4 -4
  23. package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +3 -3
  24. package/snippets/Loading/Inline-CSS-Info-and-Size.js +5 -1
  25. package/snippets/Loading/Inline-Script-Info-and-Size.js +5 -1
  26. package/snippets/Loading/JS-Execution-Time-Breakdown.js +4 -4
  27. package/snippets/Loading/Prefetch-Resource-Validation.js +12 -3
  28. package/snippets/Loading/Priority-Hints-Audit.js +10 -0
  29. package/snippets/Loading/Resource-Hints-Validation.js +3 -3
  30. package/snippets/Loading/Resource-Hints.js +7 -1
  31. package/snippets/Loading/SSR-Hydration-Data-Analysis.js +5 -2
  32. package/snippets/Loading/Script-Loading.js +6 -2
  33. package/snippets/Loading/Server-Timing-Early-Hints.js +3 -0
  34. package/snippets/Loading/Service-Worker-Analysis.js +12 -2
  35. package/snippets/Loading/Speculation-Rules-Inspector.js +7 -4
  36. package/snippets/Loading/TTFB-Resources.js +1 -1
  37. package/snippets/Loading/TTFB-Sub-Parts.js +1 -0
  38. package/snippets/Loading/Third-Party-Impact-by-Domain.js +6 -0
  39. package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +5 -0
  40. package/snippets/Loading/Webfont-Usage-Analyzer.js +13 -14
  41. package/snippets/Media/Image-Element-Audit.js +16 -13
  42. package/snippets/Media/Oversized-Images.js +4 -0
  43. package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +2 -1
  44. package/snippets/Media/Video-Element-Audit.js +13 -2
  45. package/snippets/SCHEMA.md +425 -3
  46. package/src/baseline.js +30 -6
  47. package/src/blocking.js +1 -1
  48. 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
- return {
276
- severity: "warning",
277
- message: f._redeclared
278
- ? `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}`
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
  };