webperf-snippets 1.4.0 → 1.6.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 (64) hide show
  1. package/README.md +17 -9
  2. package/package.json +1 -1
  3. package/snippets/CoreWebVitals/INP.js +6 -4
  4. package/snippets/CoreWebVitals/LCP-Image-Entropy.js +8 -9
  5. package/snippets/CoreWebVitals/LCP-Video-Candidate.js +14 -13
  6. package/snippets/Interaction/DOM-Size-and-Depth.js +4 -0
  7. package/snippets/Interaction/Forced-Synchronous-Layout.js +2 -0
  8. package/snippets/Interaction/Input-Latency-Breakdown.js +5 -2
  9. package/snippets/Interaction/Interactions.js +10 -1
  10. package/snippets/Interaction/Scroll-Performance.js +2 -0
  11. package/snippets/Loading/Back-Forward-Cache.js +7 -0
  12. package/snippets/Loading/CSS-Media-Queries-Analysis.js +4 -2
  13. package/snippets/Loading/Cache-Strategy-Analysis.js +37 -23
  14. package/snippets/Loading/Client-Side-Redirect-Detection.js +5 -3
  15. package/snippets/Loading/Compression-Audit.js +3 -0
  16. package/snippets/Loading/Content-Visibility.js +2 -2
  17. package/snippets/Loading/Critical-CSS-Detection.js +8 -1
  18. package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +2 -2
  19. package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +1 -1
  20. package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +1 -1
  21. package/snippets/Loading/Find-render-blocking-resources.js +1 -1
  22. package/snippets/Loading/First-And-Third-Party-Script-Info.js +5 -2
  23. package/snippets/Loading/First-And-Third-Party-Script-Timings.js +4 -4
  24. package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +3 -3
  25. package/snippets/Loading/Inline-CSS-Info-and-Size.js +5 -1
  26. package/snippets/Loading/Inline-Script-Info-and-Size.js +5 -1
  27. package/snippets/Loading/JS-Execution-Time-Breakdown.js +4 -4
  28. package/snippets/Loading/Prefetch-Resource-Validation.js +12 -3
  29. package/snippets/Loading/Priority-Hints-Audit.js +10 -0
  30. package/snippets/Loading/Resource-Hints-Validation.js +3 -3
  31. package/snippets/Loading/Resource-Hints.js +7 -1
  32. package/snippets/Loading/SSR-Hydration-Data-Analysis.js +5 -2
  33. package/snippets/Loading/Script-Loading.js +6 -2
  34. package/snippets/Loading/Server-Timing-Early-Hints.js +3 -0
  35. package/snippets/Loading/Service-Worker-Analysis.js +12 -2
  36. package/snippets/Loading/Speculation-Rules-Inspector.js +7 -4
  37. package/snippets/Loading/TTFB-Resources.js +1 -1
  38. package/snippets/Loading/TTFB-Sub-Parts.js +1 -0
  39. package/snippets/Loading/Third-Party-Impact-by-Domain.js +6 -0
  40. package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +5 -0
  41. package/snippets/Loading/Webfont-Usage-Analyzer.js +13 -14
  42. package/snippets/Media/Image-Element-Audit.js +16 -13
  43. package/snippets/Media/Oversized-Images.js +4 -0
  44. package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +2 -1
  45. package/snippets/Media/Video-Element-Audit.js +13 -2
  46. package/snippets/SCHEMA.md +431 -6
  47. package/snippets/_view/fields.js +37 -0
  48. package/snippets/_view/format.js +39 -0
  49. package/snippets/_view/index.js +304 -0
  50. package/snippets/_view/locales/en.js +39 -0
  51. package/snippets/_view/package.json +1 -0
  52. package/src/baseline.js +30 -6
  53. package/src/bin.js +40 -37
  54. package/src/blocking.js +1 -1
  55. package/src/budgets.js +73 -0
  56. package/src/mcp/documents.js +41 -21
  57. package/src/mcp/server.js +1 -1
  58. package/src/reporters/budgets-view.js +38 -0
  59. package/src/reporters/human.js +42 -20
  60. package/src/reporters/markdown.js +53 -1
  61. package/src/reporters/summary-view.js +12 -0
  62. package/src/runner.js +3 -3
  63. package/src/view.js +11 -0
  64. package/src/workflows/interaction.js +1 -0
@@ -53,10 +53,15 @@ Every script must return an object matching this shape:
53
53
 
54
54
  // Issues detected (for audit scripts)
55
55
  issues?: Array<{
56
+ code: string; // What the issue is, kebab-case, stable across runs and versions (see Issue codes)
56
57
  severity: "error" | "warning" | "info";
57
58
  message: string;
59
+ target?: string; // The URL, selector or name the issue is about, when there is one issue per thing
58
60
  }>;
59
61
 
62
+ // Next steps in plain text, most important first, without emoji (Cache-Strategy-Analysis)
63
+ recommendations?: string[];
64
+
60
65
  // Tracking scripts (status: "tracking")
61
66
  message?: string; // Human-readable status message
62
67
  getDataFn?: string; // window function to call for data: evaluate_script(`${getDataFn}()`). May be a dotted path such as "loafHelpers.getData"
@@ -168,7 +173,7 @@ return {
168
173
  **The window function must also return a structured object** matching the same schema, including when nothing has been recorded yet:
169
174
 
170
175
  - `status: "ok"` with `count: 0` when zero is a valid result (no long tasks, no long animation frames).
171
- - `status: "error"` with an `error` message when there is nothing to report yet and the agent needs to act first (no interactions recorded).
176
+ - `status: "tracking"` with a `message` and its `getDataFn` when there is nothing to report yet and someone has to act first (no interactions recorded): nothing failed, the snippet is still recording. `status: "error"` is for a snippet that failed.
172
177
 
173
178
  **Scripts using this pattern:** INP, Interactions, Input-Latency-Breakdown, Layout-Shift-Loading-and-Interaction, Scroll-Performance, Long-Animation-Frames (ongoing tracking), LongTask (ongoing tracking), Long-Animation-Frames-Script-Attribution.
174
179
 
@@ -260,11 +265,11 @@ Returns buffered CLS immediately and keeps tracking. Always call `getCLS()` afte
260
265
  }
261
266
  ```
262
267
 
263
- If no interactions yet, `getINP()` returns `status: "error"` with `getDataFn: "getINP"` — retry after user interaction.
268
+ If no interactions yet, `getINP()` returns `status: "tracking"` with a `message` and `getDataFn: "getINP"`: interact with the page and call it again.
264
269
 
265
270
  The phases describe the whole interaction, not one of its events. A tap fires `pointerdown`, `pointerup` and `click`, and a key press `keydown` and `keyup`, with one `interactionId`. INP, Interactions and Input-Latency-Breakdown group the entries by `interactionId`, also when they arrive in separate observer callbacks, and measure the interaction over its entries painted in the frame of the longest one: `inputDelay` runs from the first `startTime` to the first `processingStart`, `processingTime` to the last `processingEnd`, and `presentationDelay` from there to the paint (`startTime + duration`, never below zero). The interaction is reported under the event whose handler ran longest, or under the `click` or `keydown` when no handler took a millisecond more than theirs; `worstEvent`, the Interactions `type` and the Input-Latency-Breakdown event type follow that rule.
266
271
 
267
- `getINPDetails()` returns the 15 slowest interactions, slowest first, with the total in `count`. `target` is a CSS selector built like `details.element` of LCP, or `null` when the browser reports no target. Use it when `getINP()` shows poor INP and you need to identify patterns across multiple slow interactions. With no interactions yet it returns `status: "error"`, `count: 0` and no items.
272
+ `getINPDetails()` returns the 15 slowest interactions, slowest first, with the total in `count`. `target` is a CSS selector built like `details.element` of LCP, or `null` when the browser reports no target. Use it when `getINP()` shows poor INP and you need to identify patterns across multiple slow interactions. With no interactions yet it returns `status: "tracking"`, a `message`, `count: 0` and no items.
268
273
 
269
274
  ```json
270
275
  {
@@ -505,8 +510,8 @@ When the rating is not good, `issues` holds one entry that names the TTFB and th
505
510
  }
506
511
  ],
507
512
  "issues": [
508
- { "severity": "error", "message": "2 blocking scripts in <head>" },
509
- { "severity": "error", "message": "1 third-party blocking script" }
513
+ { "code": "blocking-in-head", "severity": "error", "message": "2 blocking script(s) in <head>" },
514
+ { "code": "third-party-blocking", "severity": "error", "message": "1 third-party blocking script(s)" }
510
515
  ]
511
516
  }
512
517
  ```
@@ -1189,6 +1194,424 @@ Same shape as Image-Element-Audit but for video elements.
1189
1194
 
1190
1195
  ---
1191
1196
 
1197
+ ## Issue codes
1198
+
1199
+ Every issue has a `code` that says what it is, so a comparison pairs the issues of two runs without reading their text: the message carries figures, and its wording can change from a version to the next. The code is kebab-case and unique within its snippet, so `script` and `code` together name an issue across the collection. A code never changes once published, and is never reused for another situation. The same situation keeps its code when its severity depends on a threshold.
1200
+
1201
+ An issue about one thing among several, one issue per thing, also names it in `target`: the URL of a resource, a CSS selector, or a name such as an event type or a CSS property. Two issues with the same code and different targets are two issues.
1202
+
1203
+ ### LCP-Image-Entropy
1204
+
1205
+ | Code | Severity | When |
1206
+ |------|----------|------|
1207
+ | `low-entropy-images` | warning | Images under 0.05 bits per pixel, which Chrome does not take as the LCP |
1208
+ | `lcp-low-entropy` | error | The LCP image itself has low entropy |
1209
+ | `lcp-size-unknown` | info | The LCP `<img>` has no file size to read (a `data:` URL, or cross-origin without `Timing-Allow-Origin`) |
1210
+ | `lcp-not-img` | info | The LCP is the image of another element, such as a CSS background or a video poster |
1211
+ | `lcp-is-text` | info | The LCP element is text |
1212
+
1213
+ ### LCP-Video-Candidate
1214
+
1215
+ | Code | Severity | When |
1216
+ |------|----------|------|
1217
+ | `lcp-not-video` | info | The LCP element is not a `<video>` |
1218
+ | `lcp-element-removed` | info | The LCP element is no longer in the DOM |
1219
+ | `late-candidate` | warning | The LCP candidate rendered more than 10 s after the load |
1220
+ | `first-frame-lcp` | info | The LCP is the first frame of the video, without a poster |
1221
+ | `first-frame-not-autoplay-muted` | warning | A first-frame LCP on a video without `autoplay` and `muted` |
1222
+ | `poster-not-preloaded` | warning | The poster has no `<link rel="preload" as="image">` |
1223
+ | `poster-preload-no-fetchpriority` | info | The preload of the poster has no `fetchpriority="high"` |
1224
+ | `poster-legacy-format` | info | The poster is not AVIF or WebP |
1225
+ | `render-time-hidden` | info | `renderTime` is 0: the video is cross-origin without `Timing-Allow-Origin` |
1226
+ | `preload-none-delays-poster` | warning | `preload="none"` on a video without `autoplay` |
1227
+
1228
+ ### Script-Loading
1229
+
1230
+ | Code | Severity | When |
1231
+ |------|----------|------|
1232
+ | `blocking-in-head` | error | Classic scripts without `async`, `defer` or `type="module"` in `<head>` |
1233
+ | `third-party-blocking` | error | Blocking scripts from a third party |
1234
+ | `large-blocking` | warning | Blocking scripts larger than 50 KB |
1235
+ | `could-defer` | info | Blocking scripts other than the last three of `<body>`, which could try `defer` |
1236
+ | `unknown-size` | info | Scripts whose size is hidden by a missing `Timing-Allow-Origin`, so the total is a lower bound |
1237
+
1238
+ ### CSS-Media-Queries-Analysis
1239
+
1240
+ | Code | Severity | When |
1241
+ |------|----------|------|
1242
+ | `no-wide-rules` | info | No `@media` rules for viewports wider than the breakpoint (768 px by default) |
1243
+ | `wide-rules` | info or warning | `@media` rules for wider viewports that narrower screens download without using, a warning above 50 KB |
1244
+ | `repeated-queries` | info | Few distinct queries repeated across many `@media` blocks |
1245
+ | `unreadable-stylesheets` | info | Cross-origin stylesheets whose rules cannot be read |
1246
+
1247
+ ### Content-Visibility
1248
+
1249
+ | Code | Severity | When |
1250
+ |------|----------|------|
1251
+ | `missing-intrinsic-size` | warning | An element with `content-visibility: auto` and no `contain-intrinsic-size`; `target` is its selector |
1252
+ | `not-used` | info | No `content-visibility` on the page |
1253
+
1254
+ ### Critical-CSS-Detection
1255
+
1256
+ | Code | Severity | When |
1257
+ |------|----------|------|
1258
+ | `no-inline-critical-css` | error | Render-blocking stylesheets and no inline critical CSS |
1259
+ | `render-blocking-over-budget` | error | Render-blocking CSS above the 14 KB critical budget |
1260
+ | `large-not-preloaded` | warning | Render-blocking stylesheets above 10 KB without preload |
1261
+ | `inline-over-budget` | error | Inline CSS above the 14 KB critical budget |
1262
+ | `styles-in-body` | warning | Inline style blocks in `<body>` |
1263
+
1264
+ ### Find-Above-The-Fold-Lazy-Loaded-Images
1265
+
1266
+ | Code | Severity | When |
1267
+ |------|----------|------|
1268
+ | `lcp-lazy` | error | The LCP candidate image has `loading="lazy"` |
1269
+ | `lazy-above-fold` | warning | Other images above the fold with `loading="lazy"` |
1270
+
1271
+ ### Find-Images-With-Lazy-and-Fetchpriority
1272
+
1273
+ | Code | Severity | When |
1274
+ |------|----------|------|
1275
+ | `lazy-and-high-priority` | error | Elements with both `loading="lazy"` and `fetchpriority="high"` |
1276
+
1277
+ ### Find-non-Lazy-Loaded-Images-outside-of-the-viewport
1278
+
1279
+ | Code | Severity | When |
1280
+ |------|----------|------|
1281
+ | `eager-off-viewport` | warning | Images outside the viewport without `loading="lazy"` |
1282
+
1283
+ ### Find-render-blocking-resources
1284
+
1285
+ | Code | Severity | When |
1286
+ |------|----------|------|
1287
+ | `render-blocking` | error | Resources that block rendering |
1288
+
1289
+ ### Fonts-Preloaded-Loaded-and-used-above-the-fold
1290
+
1291
+ Each issue names its font in `target`.
1292
+
1293
+ | Code | Severity | When |
1294
+ |------|----------|------|
1295
+ | `preloaded-unused` | warning | A preloaded font that the content above the fold does not use |
1296
+ | `used-not-preloaded` | warning | A font used above the fold without preload |
1297
+ | `preload-no-crossorigin` | error | A font preload without `crossorigin`, which fetches the font twice |
1298
+
1299
+ ### Inline-CSS-Info-and-Size
1300
+
1301
+ | Code | Severity | When |
1302
+ |------|----------|------|
1303
+ | `over-budget` | warning | Inline CSS above the 14 KB critical budget |
1304
+ | `duplicate-blocks` | warning | Style blocks with identical content |
1305
+ | `large-blocks` | warning | Style blocks above 5 KB |
1306
+ | `styles-in-body` | warning | Style blocks in `<body>` |
1307
+
1308
+ ### Webfont-Usage-Analyzer
1309
+
1310
+ `load-failed`, `loaded-unused` and `redeclared` name the font file by its URL in `target`, or the face when it has none.
1311
+
1312
+ | Code | Severity | When |
1313
+ |------|----------|------|
1314
+ | `load-failed` | error | A font that failed to load |
1315
+ | `loaded-unused` | warning | A loaded font that no rendered element uses |
1316
+ | `redeclared` | warning | A loaded font declared again later with the same descriptors, so the page uses the other file |
1317
+ | `blocking-font-display` | warning | Faces with `font-display` `auto` or `block`, which can keep text invisible |
1318
+ | `heavy-fonts` | warning | Web fonts above 300 KB in total |
1319
+ | `never-loaded` | info | Faces declared and never loaded |
1320
+ | `shadowed` | info | Faces declared more than once, whose earlier declarations never loaded |
1321
+ | `unused-subsets` | info | `unicode-range` subsets the page did not need |
1322
+ | `size-unknown` | info | Cross-origin font files without `Timing-Allow-Origin`, so the byte totals are a lower bound |
1323
+ | `unreadable-stylesheets` | info | Cross-origin stylesheets whose font files cannot be matched to faces |
1324
+ | `elements-capped` | info | Only the first 20,000 elements were scanned for text |
1325
+
1326
+ ### First-And-Third-Party-Script-Info
1327
+
1328
+ | Code | Severity | When |
1329
+ |------|----------|------|
1330
+ | `unknown-size` | info | Scripts whose size is hidden by a missing `Timing-Allow-Origin`, so the totals are a lower bound |
1331
+ | `render-blocking-unsupported` | info | The browser has no `renderBlockingStatus` (only Chromium has it) |
1332
+ | `third-party-blocking` | error | Third-party scripts that block rendering |
1333
+ | `many-third-party-hosts` | warning | Scripts from more than 3 third-party hosts |
1334
+ | `large-third-party` | warning | Third-party scripts above 100 KB in total |
1335
+
1336
+ ### First-And-Third-Party-Script-Timings
1337
+
1338
+ | Code | Severity | When |
1339
+ |------|----------|------|
1340
+ | `slow-scripts` | warning | Scripts that take more than 500 ms to load |
1341
+ | `slow-dns` | warning | A DNS lookup above 100 ms |
1342
+ | `slow-connection` | warning | A TCP connection above 100 ms |
1343
+ | `slow-server` | warning | A server response above 200 ms |
1344
+
1345
+ ### Inline-Script-Info-and-Size
1346
+
1347
+ | Code | Severity | When |
1348
+ |------|----------|------|
1349
+ | `parser-blocking-in-head` | error | Inline scripts in `<head>` that block the parser |
1350
+ | `large-inline` | warning | Inline scripts above 2 KB |
1351
+ | `duplicate-scripts` | warning | Inline scripts with identical content |
1352
+ | `analytics-in-head` | warning | Analytics scripts that block in `<head>` |
1353
+
1354
+ ### JS-Execution-Time-Breakdown
1355
+
1356
+ | Code | Severity | When |
1357
+ |------|----------|------|
1358
+ | `render-blocking` | error | Scripts that block HTML parsing |
1359
+ | `high-parse-cost` | warning | An estimated parse cost above 500 ms on mobile |
1360
+ | `split-candidates` | warning | Scripts above 50 KB decoded, candidates for code splitting |
1361
+ | `low-compression` | warning | A compression ratio below 2x on more than 10 KB of scripts |
1362
+
1363
+ ### SSR-Hydration-Data-Analysis
1364
+
1365
+ | Code | Severity | When |
1366
+ |------|----------|------|
1367
+ | `large-hydration-data` | warning | The hydration data of a framework above its threshold; `target` is the framework |
1368
+ | `large-arrays` | warning | `pageProps` arrays with more than 50 entries |
1369
+ | `deep-nesting` | warning | `pageProps` nested more than 5 levels |
1370
+ | `sensitive-keys` | warning | `pageProps` keys that look sensitive |
1371
+
1372
+ ### Third-Party-Impact-by-Domain
1373
+
1374
+ | Code | Severity | When |
1375
+ |------|----------|------|
1376
+ | `render-blocking-domains` | warning | Third-party domains that serve render-blocking resources |
1377
+ | `heavy-domain` | warning | A domain whose scripts take more than 250 ms in long animation frames; `target` is the domain |
1378
+ | `render-blocking-unsupported` | info | The browser has no `renderBlockingStatus` (only Chromium has it) |
1379
+ | `loaf-unattributed` | info | Long animation frames with no third-party script attributed |
1380
+ | `unknown-size` | info | Third-party requests without `Timing-Allow-Origin`, so the transfer size is a lower bound |
1381
+
1382
+ ### Validate-Preload-Async-Defer-Scripts
1383
+
1384
+ Each issue names the preloaded script in `target`.
1385
+
1386
+ | Code | Severity | When |
1387
+ |------|----------|------|
1388
+ | `orphan-preload` | warning | A preloaded script that the page neither has nor loaded |
1389
+ | `async-defer-preload` | error | A script with `async` or `defer` that is also preloaded, which raises its priority |
1390
+ | `module-preload` | warning | An ES module preloaded with `rel="preload"` instead of `rel="modulepreload"` |
1391
+
1392
+ ### Back-Forward-Cache
1393
+
1394
+ | Code | Severity | When |
1395
+ |------|----------|------|
1396
+ | `not-restored` | error | A reason the browser gave for not restoring the page from bfcache, after a back or forward navigation; `target` is the reason and its frame |
1397
+ | `unload-handler` | error | A `window.onunload` handler |
1398
+ | `cache-control-no-store` | warning | `Cache-Control: no-store` on the page response |
1399
+ | `iframes` | info | Iframes on the page, whose blockers block the whole page |
1400
+ | `service-worker` | info | A Service Worker controls the page, which bfcache allows |
1401
+
1402
+ ### Cache-Strategy-Analysis
1403
+
1404
+ Each issue names its resource in `target`, except `header-coverage`. `id` repeats the code, as before the codes. `recommendations` lists the next steps in plain text, the most important first; the console marks their priority with 🔴, 🟡 and ℹ️.
1405
+
1406
+ | Code | Severity | When |
1407
+ |------|----------|------|
1408
+ | `static-no-store` | error | A static asset with `no-store` |
1409
+ | `versioned-short-cache` | warning | A URL with a hash and a short `max-age` |
1410
+ | `versioned-not-immutable` | info | A URL with a hash and no `immutable` |
1411
+ | `large-no-cache` | error | A large resource with no effective cache |
1412
+ | `expires-without-cc` | warning | An `Expires` header without `Cache-Control` |
1413
+ | `maxage-zero-static` | warning | A static asset with `max-age=0` |
1414
+ | `short-cache-static` | warning | A static asset with a `max-age` under an hour |
1415
+ | `uncompressed` | warning | A text resource sent uncompressed |
1416
+ | `duplicate-uncached` | warning | A resource loaded several times from the network |
1417
+ | `header-coverage` | info | The share of resources whose cache headers could be read |
1418
+
1419
+ ### Client-Side-Redirect-Detection
1420
+
1421
+ | Code | Severity | When |
1422
+ |------|----------|------|
1423
+ | `server-redirects` | warning | Server-side redirects before the document |
1424
+ | `document-navigation` | error | Same-origin document navigations, likely a client-side redirect |
1425
+ | `same-origin-navigation` | warning | A same-origin referrer with another path |
1426
+ | `fast-minimal-navigation` | warning | A small same-origin page that loads fast, possibly a redirect page |
1427
+
1428
+ ### Compression-Audit
1429
+
1430
+ | Code | Severity | When |
1431
+ |------|----------|------|
1432
+ | `uncompressed` | warning | Text resources served without compression |
1433
+ | `unknown-size` | info | Cross-origin text resources without `Timing-Allow-Origin`, whose compression is unknown |
1434
+ | `content-encoding-unavailable` | info | No `contentEncoding` in Resource Timing, so compression is inferred from the sizes |
1435
+
1436
+ ### Prefetch-Resource-Validation
1437
+
1438
+ Each issue about one prefetch names its URL in `target`.
1439
+
1440
+ | Code | Severity | When |
1441
+ |------|----------|------|
1442
+ | `no-prefetch` | info | No `rel="prefetch"` hints on the page |
1443
+ | `missing-as` | warning | A prefetch without `as` |
1444
+ | `invalid-as` | warning | A prefetch with an `as` value that does not exist |
1445
+ | `duplicate-prefetch` | warning | A URL prefetched more than once |
1446
+ | `used-on-current-page` | error | A prefetched resource that the current page uses, which needs `preload` |
1447
+ | `large-file` | warning | A prefetched file above 500 KB |
1448
+ | `inappropriate-type` | warning | A prefetched type that may not suit prefetch |
1449
+ | `too-many-prefetches` | warning | More than 10 prefetch hints |
1450
+ | `large-total-size` | warning or error | Prefetches above 2 MB in total, an error above 5 MB |
1451
+
1452
+ ### Priority-Hints-Audit
1453
+
1454
+ Each issue names its resource in `target`.
1455
+
1456
+ | Code | Severity | When |
1457
+ |------|----------|------|
1458
+ | `preload-low-priority` | error | A preload with `fetchpriority="low"` |
1459
+ | `low-priority-loaded-early` | warning | A `fetchpriority="low"` resource that still loaded early |
1460
+ | `high-priority-loaded-late` | warning | A `fetchpriority="high"` resource that still loaded late |
1461
+ | `lcp-no-fetchpriority` | warning | The LCP candidate image without `fetchpriority="high"` |
1462
+
1463
+ ### Resource-Hints
1464
+
1465
+ Each issue about one hint names its URL in `target`.
1466
+
1467
+ | Code | Severity | When |
1468
+ |------|----------|------|
1469
+ | `preload-missing-as` | error | A preload without `as` |
1470
+ | `font-preload-no-crossorigin` | error | A font preload without `crossorigin`, which fetches the font twice |
1471
+ | `preconnect-no-crossorigin` | warning | A preconnect without `crossorigin` to an origin that serves CORS resources |
1472
+ | `unused-preload` | warning | A preload that the page did not use during the load |
1473
+ | `unused-preconnect` | warning | A preconnect to an origin the page loaded nothing from |
1474
+ | `too-many-preloads` | warning | More than 6 preloads |
1475
+
1476
+ ### Resource-Hints-Validation
1477
+
1478
+ | Code | Severity | When |
1479
+ |------|----------|------|
1480
+ | `unused-hints` | warning | Hints the page did not use |
1481
+ | `missing-connection-hints` | info | Domains that could use a connection hint |
1482
+ | `redundant-hints` | warning | Domains with both `preconnect` and `dns-prefetch` |
1483
+
1484
+ ### Server-Timing-Early-Hints
1485
+
1486
+ | Code | Severity | When |
1487
+ |------|----------|------|
1488
+ | `no-server-timing` | info | The document response has no `Server-Timing` header |
1489
+ | `timing-hidden` | info | Cross-origin resources without `Timing-Allow-Origin`, whose Server-Timing cannot be read |
1490
+ | `no-early-hints` | info | No 103 Early Hints response for the document |
1491
+
1492
+ ### Service-Worker-Analysis
1493
+
1494
+ | Code | Severity | When |
1495
+ |------|----------|------|
1496
+ | `unsupported` | error | The browser has no Service Workers |
1497
+ | `no-service-worker` | info | No Service Worker registered |
1498
+ | `not-controlled` | warning | The page is not controlled by its Service Worker, after a hard reload or on a first visit |
1499
+ | `update-pending` | warning | A waiting update; `target` is the scope |
1500
+ | `navigation-preload-disabled` | info | Navigation Preload is off; `target` is the scope |
1501
+ | `slow-startup` | warning or error | A Service Worker startup above 50 ms, an error above 100 ms |
1502
+ | `low-cache-hit-rate` | warning or error | A cache hit rate under 80%, an error under 50% |
1503
+ | `unknown-source` | info | Intercepted cross-origin resources without `Timing-Allow-Origin`, left out of the hit rate |
1504
+
1505
+ ### Speculation-Rules-Inspector
1506
+
1507
+ | Code | Severity | When |
1508
+ |------|----------|------|
1509
+ | `invalid-json` | error | A speculation rules script that is not valid JSON |
1510
+ | `eager-list-capped` | warning | An eager prerender list with more than 10 URLs, above what Chrome prerenders |
1511
+ | `eager-document-rule` | warning | A document prerender rule with eager eagerness |
1512
+ | `no-rules` | info | No speculation rules in the DOM |
1513
+ | `no-prefetch-or-prerender` | info | Speculation rules scripts without prefetch or prerender rules |
1514
+ | `unsupported` | info | The browser does not support speculation rules |
1515
+ | `prerendered` | info | The page was prerendered, so its timings start at `activationStart` |
1516
+
1517
+ ### TTFB-Resources
1518
+
1519
+ | Code | Severity | When |
1520
+ |------|----------|------|
1521
+ | `no-resources` | info | No resources with TTFB data |
1522
+
1523
+ ### TTFB-Sub-Parts
1524
+
1525
+ | Code | Severity | When |
1526
+ |------|----------|------|
1527
+ | `slow-ttfb` | warning or error | A TTFB that is not good, an error when poor, naming its slowest phase |
1528
+
1529
+ ### DOM-Size-and-Depth
1530
+
1531
+ | Code | Severity | When |
1532
+ |------|----------|------|
1533
+ | `too-many-nodes` | warning or error | More than 800 nodes, an error above 1,400 |
1534
+ | `too-deep` | warning | A DOM deeper than 32 levels |
1535
+ | `too-many-children` | warning | An element with more than 60 children |
1536
+
1537
+ ### Forced-Synchronous-Layout
1538
+
1539
+ | Code | Severity | When |
1540
+ |------|----------|------|
1541
+ | `forced-layout` | warning or error | A layout property read after a write forced a layout, an error above 5 times; `target` is the property |
1542
+
1543
+ ### Input-Latency-Breakdown
1544
+
1545
+ | Code | Severity | When |
1546
+ |------|----------|------|
1547
+ | `slow-event-type` | warning or error | An event type whose P75 is not good, an error when poor; `target` is the event type |
1548
+
1549
+ ### Interactions
1550
+
1551
+ | Code | Severity | When |
1552
+ |------|----------|------|
1553
+ | `slow-interaction` | warning or error | An interaction above 200 ms, an error above 500 ms; `target` is its type |
1554
+
1555
+ ### Scroll-Performance
1556
+
1557
+ | Code | Severity | When |
1558
+ |------|----------|------|
1559
+ | `non-passive-listeners` | warning | Scroll or touch listeners without `{ passive: true }` |
1560
+ | `will-change` | info | Elements with `will-change` set |
1561
+
1562
+ ### Image-Element-Audit
1563
+
1564
+ Each issue names its image in `target`, except `lcp-not-img`.
1565
+
1566
+ | Code | Severity | When |
1567
+ |------|----------|------|
1568
+ | `lcp-not-img` | info | The LCP element is not an `<img>` |
1569
+ | `lcp-no-fetchpriority` | error | The LCP image has no `fetchpriority="high"` |
1570
+ | `lcp-lazy` | error | The LCP image has `loading="lazy"` |
1571
+ | `lcp-not-decoding-sync` | warning | The LCP image has no `decoding="sync"` |
1572
+ | `lcp-not-preloaded` | warning | The LCP image has no `<link rel="preload" as="image">` |
1573
+ | `lcp-preload-no-fetchpriority` | info | The preload of the LCP image has no `fetchpriority="high"` |
1574
+ | `lazy-in-viewport` | warning | An image in the viewport with `loading="lazy"` |
1575
+ | `eager-off-viewport` | warning | An image outside the viewport without `loading="lazy"` |
1576
+ | `high-priority-off-viewport` | error | An image outside the viewport with `fetchpriority="high"` |
1577
+ | `lazy-and-high-priority` | error | An image with both `loading="lazy"` and `fetchpriority="high"` |
1578
+ | `missing-dimensions` | warning | An image without `width` and `height` |
1579
+ | `no-modern-format` | info | An image that is not WebP, AVIF, JPEG XL or SVG, outside a `<picture>` |
1580
+
1581
+ ### Oversized-Images
1582
+
1583
+ Each issue names its image in `target`.
1584
+
1585
+ | Code | Severity | When |
1586
+ |------|----------|------|
1587
+ | `oversized` | info or warning | A file larger than its slot needs, a warning when it is far larger |
1588
+ | `undersized` | info or warning | A file smaller than its slot needs, so it looks blurry, a warning when it is far smaller |
1589
+
1590
+ ### SVG-Embedded-Bitmap-Analysis
1591
+
1592
+ | Code | Severity | When |
1593
+ |------|----------|------|
1594
+ | `embedded-bitmaps` | warning | SVG files that contain bitmaps |
1595
+ | `sprite-opportunity` | info | Standalone inline SVGs that could share one `<symbol>` sprite |
1596
+
1597
+ ### Video-Element-Audit
1598
+
1599
+ Each issue names its video in `target`, except `no-videos`.
1600
+
1601
+ | Code | Severity | When |
1602
+ |------|----------|------|
1603
+ | `no-videos` | info | No `<video>` elements on the page |
1604
+ | `preload-auto` | error | `preload="auto"` on a video without `autoplay` |
1605
+ | `off-viewport-preload` | warning | A video outside the viewport without `preload="none"` |
1606
+ | `autoplay-not-muted` | error | `autoplay` without `muted` |
1607
+ | `autoplay-no-playsinline` | warning | `autoplay` without `playsinline` |
1608
+ | `missing-poster` | warning | A video without a poster |
1609
+ | `missing-dimensions` | warning | A video without `width` and `height` |
1610
+ | `no-modern-codec` | info | No WebM source (VP9 or AV1) |
1611
+ | `no-controls` | info | A video without `controls` and without `autoplay` |
1612
+
1613
+ ---
1614
+
1192
1615
  ## Guidelines for Agents
1193
1616
 
1194
1617
  ### Reading results
@@ -1223,6 +1646,7 @@ data = evaluate_script("getINP()")
1223
1646
  - `rating === "poor"` → high priority fix, check `issues` for specific problems
1224
1647
  - `count > 0` and `issues.length > 0` → audit found actionable problems
1225
1648
  - `count === 0` → nothing to audit (no render-blocking resources, no images, etc.)
1649
+ - To tell whether an issue is new, fixed or the same as in an earlier run, compare `script`, `code` and `target`, not `message`
1226
1650
 
1227
1651
  ---
1228
1652
 
@@ -1240,5 +1664,6 @@ data = evaluate_script("getINP()")
1240
1664
  10. **Valid statuses only** — `ok`, `tracking`, `error` or `unsupported`, with an `error` message when the status is `error` or `unsupported`.
1241
1665
  11. **Hidden cross-origin data is reported, not guessed** — a resource whose timing or size is hidden (no `Timing-Allow-Origin`) reports zeros for every size and timing. Count it (`sizeKnown: false`, `corsRestrictedCount`) instead of treating the zero as a measurement.
1242
1666
  12. **Partial analysis is flagged** — when hidden cross-origin data leaves part of the set unanalyzed, set the top-level `corsLimitedAnalysis: true` and report how many resources were skipped in `details` (`sizeUnknownCount`, `corsRestrictedCount`). Set it to `false` when every resource was analyzed.
1667
+ 13. **Issues carry a stable code**: every issue has a kebab-case `code`, written as a literal in the source (`code: "blocking-in-head"`) and listed under Issue codes, and a `target` string when it is about one resource.
1243
1668
 
1244
- The rules are checked by `cli/tests/e2e/snippet-contract.test.js`, which runs every script, its `getDataFn` and every other getter it exposes on `window` (such as `getINPDetails()` and `checkBfcache()`), on an empty page, a seeded page and a heavy page.
1669
+ The rules are checked by `cli/tests/e2e/snippet-contract.test.js`, which runs every script, its `getDataFn` and every other getter it exposes on `window` (such as `getINPDetails()` and `checkBfcache()`), on an empty page, a seeded page and a heavy page. `cli/tests/unit/issue-codes.test.js` checks that the codes in the source of each snippet match its table under Issue codes.
@@ -0,0 +1,37 @@
1
+ // How each field of a result is shown, the same in every language: its format, its place among the
2
+ // others (a lower order comes first; a field without one keeps its place after them) and whether it is
3
+ // hidden. The entries of a snippet win over the general ones under "*". A field without an entry takes
4
+ // its format from the suffix of its key (format.js). The words are in locales/.
5
+
6
+ export default {
7
+ '*': {
8
+ // A CSS selector of an element of the page, which a consumer can make inspectable
9
+ selector: { format: 'selector' },
10
+ element: { format: 'selector' },
11
+ lcpElement: { format: 'selector' },
12
+ finalElement: { format: 'selector' },
13
+ // The name of a phase, read as its label
14
+ slowestPhase: { format: 'key' },
15
+ // The issues of a row: the issues section of the result lists them already
16
+ issues: { hidden: true },
17
+ },
18
+ 'Find-Above-The-Fold-Lazy-Loaded-Images': {
19
+ // Its top and left in px: the distance from the edge of the viewport says what matters
20
+ position: { hidden: true },
21
+ },
22
+ 'Input-Latency-Breakdown': {
23
+ // The same as its rows, one per event type
24
+ eventTypes: { hidden: true },
25
+ },
26
+ INP: {
27
+ phases: { format: 'phases' },
28
+ },
29
+ 'Find-render-blocking-resources': {
30
+ shortName: { order: 1 },
31
+ type: { order: 2 },
32
+ responseEndMs: { order: 3 },
33
+ durationMs: { order: 4 },
34
+ sizeBytes: { order: 5 },
35
+ url: { order: 6 },
36
+ },
37
+ }
@@ -0,0 +1,39 @@
1
+ // The formats of the values of a result, with the numbers written as the locale writes them. A field
2
+ // without a format in fields.js takes it from the suffix of its key.
3
+
4
+ const KB = 1024
5
+ const MB = 1024 * 1024
6
+
7
+ const number = (value, locale, digits = 0, minimum = 0) =>
8
+ new Intl.NumberFormat(locale, { maximumFractionDigits: digits, minimumFractionDigits: minimum }).format(value)
9
+
10
+ const FORMATS = {
11
+ ms: (value, locale) => `${number(Math.round(value), locale)} ms`,
12
+ bytes: (value, locale) =>
13
+ value >= MB ? `${number(value / MB, locale, 1, 1)} MB` : value >= KB ? `${number(value / KB, locale, 1, 1)} KB` : `${number(value, locale)} B`,
14
+ percent: (value, locale) => new Intl.NumberFormat(locale, { style: 'unit', unit: 'percent', maximumFractionDigits: 1 }).format(value),
15
+ count: (value, locale) => number(value, locale),
16
+ score: (value, locale) => number(value, locale, 3, 3),
17
+ bpp: (value, locale) => `${number(value, locale, 3, 3)} bpp`,
18
+ fps: (value, locale) => `${number(Math.round(value), locale)} fps`,
19
+ px: (value, locale) => `${number(Math.round(value), locale)} px`,
20
+ }
21
+
22
+ // The suffixes that name a unit, and the format each one gives
23
+ export const SUFFIXES = [
24
+ ['Ms', 'ms'],
25
+ ['Bytes', 'bytes'],
26
+ ['Percent', 'percent'],
27
+ ['Count', 'count'],
28
+ ['Px', 'px'],
29
+ ]
30
+
31
+ export const formatOfKey = (key) => SUFFIXES.find(([suffix]) => key.endsWith(suffix) && key !== suffix)?.[1]
32
+
33
+ // Text as it is, a number in its format, and an object or a list as compact JSON
34
+ export function formatValue(value, format, locale = 'en') {
35
+ if (value === undefined || value === null) return ''
36
+ if (typeof value === 'number') return (FORMATS[format] ?? ((v, l) => number(v, l, 3)))(value, locale)
37
+ if (typeof value === 'object') return JSON.stringify(value)
38
+ return String(value)
39
+ }