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
package/snippets/SCHEMA.md
CHANGED
|
@@ -53,8 +53,10 @@ 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
|
|
|
60
62
|
// Tracking scripts (status: "tracking")
|
|
@@ -505,8 +507,8 @@ When the rating is not good, `issues` holds one entry that names the TTFB and th
|
|
|
505
507
|
}
|
|
506
508
|
],
|
|
507
509
|
"issues": [
|
|
508
|
-
{ "severity": "error", "message": "2 blocking
|
|
509
|
-
{ "severity": "error", "message": "1 third-party blocking script" }
|
|
510
|
+
{ "code": "blocking-in-head", "severity": "error", "message": "2 blocking script(s) in <head>" },
|
|
511
|
+
{ "code": "third-party-blocking", "severity": "error", "message": "1 third-party blocking script(s)" }
|
|
510
512
|
]
|
|
511
513
|
}
|
|
512
514
|
```
|
|
@@ -1189,6 +1191,424 @@ Same shape as Image-Element-Audit but for video elements.
|
|
|
1189
1191
|
|
|
1190
1192
|
---
|
|
1191
1193
|
|
|
1194
|
+
## Issue codes
|
|
1195
|
+
|
|
1196
|
+
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.
|
|
1197
|
+
|
|
1198
|
+
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.
|
|
1199
|
+
|
|
1200
|
+
### LCP-Image-Entropy
|
|
1201
|
+
|
|
1202
|
+
| Code | Severity | When |
|
|
1203
|
+
|------|----------|------|
|
|
1204
|
+
| `low-entropy-images` | warning | Images under 0.05 bits per pixel, which Chrome does not take as the LCP |
|
|
1205
|
+
| `lcp-low-entropy` | error | The LCP image itself has low entropy |
|
|
1206
|
+
| `lcp-size-unknown` | info | The LCP `<img>` has no file size to read (a `data:` URL, or cross-origin without `Timing-Allow-Origin`) |
|
|
1207
|
+
| `lcp-not-img` | info | The LCP is the image of another element, such as a CSS background or a video poster |
|
|
1208
|
+
| `lcp-is-text` | info | The LCP element is text |
|
|
1209
|
+
|
|
1210
|
+
### LCP-Video-Candidate
|
|
1211
|
+
|
|
1212
|
+
| Code | Severity | When |
|
|
1213
|
+
|------|----------|------|
|
|
1214
|
+
| `lcp-not-video` | info | The LCP element is not a `<video>` |
|
|
1215
|
+
| `lcp-element-removed` | info | The LCP element is no longer in the DOM |
|
|
1216
|
+
| `late-candidate` | warning | The LCP candidate rendered more than 10 s after the load |
|
|
1217
|
+
| `first-frame-lcp` | info | The LCP is the first frame of the video, without a poster |
|
|
1218
|
+
| `first-frame-not-autoplay-muted` | warning | A first-frame LCP on a video without `autoplay` and `muted` |
|
|
1219
|
+
| `poster-not-preloaded` | warning | The poster has no `<link rel="preload" as="image">` |
|
|
1220
|
+
| `poster-preload-no-fetchpriority` | info | The preload of the poster has no `fetchpriority="high"` |
|
|
1221
|
+
| `poster-legacy-format` | info | The poster is not AVIF or WebP |
|
|
1222
|
+
| `render-time-hidden` | info | `renderTime` is 0: the video is cross-origin without `Timing-Allow-Origin` |
|
|
1223
|
+
| `preload-none-delays-poster` | warning | `preload="none"` on a video without `autoplay` |
|
|
1224
|
+
|
|
1225
|
+
### Script-Loading
|
|
1226
|
+
|
|
1227
|
+
| Code | Severity | When |
|
|
1228
|
+
|------|----------|------|
|
|
1229
|
+
| `blocking-in-head` | error | Classic scripts without `async`, `defer` or `type="module"` in `<head>` |
|
|
1230
|
+
| `third-party-blocking` | error | Blocking scripts from a third party |
|
|
1231
|
+
| `large-blocking` | warning | Blocking scripts larger than 50 KB |
|
|
1232
|
+
| `could-defer` | info | Blocking scripts other than the last three of `<body>`, which could try `defer` |
|
|
1233
|
+
| `unknown-size` | info | Scripts whose size is hidden by a missing `Timing-Allow-Origin`, so the total is a lower bound |
|
|
1234
|
+
|
|
1235
|
+
### CSS-Media-Queries-Analysis
|
|
1236
|
+
|
|
1237
|
+
| Code | Severity | When |
|
|
1238
|
+
|------|----------|------|
|
|
1239
|
+
| `no-wide-rules` | info | No `@media` rules for viewports wider than the breakpoint (768 px by default) |
|
|
1240
|
+
| `wide-rules` | info or warning | `@media` rules for wider viewports that narrower screens download without using, a warning above 50 KB |
|
|
1241
|
+
| `repeated-queries` | info | Few distinct queries repeated across many `@media` blocks |
|
|
1242
|
+
| `unreadable-stylesheets` | info | Cross-origin stylesheets whose rules cannot be read |
|
|
1243
|
+
|
|
1244
|
+
### Content-Visibility
|
|
1245
|
+
|
|
1246
|
+
| Code | Severity | When |
|
|
1247
|
+
|------|----------|------|
|
|
1248
|
+
| `missing-intrinsic-size` | warning | An element with `content-visibility: auto` and no `contain-intrinsic-size`; `target` is its selector |
|
|
1249
|
+
| `not-used` | info | No `content-visibility` on the page |
|
|
1250
|
+
|
|
1251
|
+
### Critical-CSS-Detection
|
|
1252
|
+
|
|
1253
|
+
| Code | Severity | When |
|
|
1254
|
+
|------|----------|------|
|
|
1255
|
+
| `no-inline-critical-css` | error | Render-blocking stylesheets and no inline critical CSS |
|
|
1256
|
+
| `render-blocking-over-budget` | error | Render-blocking CSS above the 14 KB critical budget |
|
|
1257
|
+
| `large-not-preloaded` | warning | Render-blocking stylesheets above 10 KB without preload |
|
|
1258
|
+
| `inline-over-budget` | error | Inline CSS above the 14 KB critical budget |
|
|
1259
|
+
| `styles-in-body` | warning | Inline style blocks in `<body>` |
|
|
1260
|
+
|
|
1261
|
+
### Find-Above-The-Fold-Lazy-Loaded-Images
|
|
1262
|
+
|
|
1263
|
+
| Code | Severity | When |
|
|
1264
|
+
|------|----------|------|
|
|
1265
|
+
| `lcp-lazy` | error | The LCP candidate image has `loading="lazy"` |
|
|
1266
|
+
| `lazy-above-fold` | warning | Other images above the fold with `loading="lazy"` |
|
|
1267
|
+
|
|
1268
|
+
### Find-Images-With-Lazy-and-Fetchpriority
|
|
1269
|
+
|
|
1270
|
+
| Code | Severity | When |
|
|
1271
|
+
|------|----------|------|
|
|
1272
|
+
| `lazy-and-high-priority` | error | Elements with both `loading="lazy"` and `fetchpriority="high"` |
|
|
1273
|
+
|
|
1274
|
+
### Find-non-Lazy-Loaded-Images-outside-of-the-viewport
|
|
1275
|
+
|
|
1276
|
+
| Code | Severity | When |
|
|
1277
|
+
|------|----------|------|
|
|
1278
|
+
| `eager-off-viewport` | warning | Images outside the viewport without `loading="lazy"` |
|
|
1279
|
+
|
|
1280
|
+
### Find-render-blocking-resources
|
|
1281
|
+
|
|
1282
|
+
| Code | Severity | When |
|
|
1283
|
+
|------|----------|------|
|
|
1284
|
+
| `render-blocking` | error | Resources that block rendering |
|
|
1285
|
+
|
|
1286
|
+
### Fonts-Preloaded-Loaded-and-used-above-the-fold
|
|
1287
|
+
|
|
1288
|
+
Each issue names its font in `target`.
|
|
1289
|
+
|
|
1290
|
+
| Code | Severity | When |
|
|
1291
|
+
|------|----------|------|
|
|
1292
|
+
| `preloaded-unused` | warning | A preloaded font that the content above the fold does not use |
|
|
1293
|
+
| `used-not-preloaded` | warning | A font used above the fold without preload |
|
|
1294
|
+
| `preload-no-crossorigin` | error | A font preload without `crossorigin`, which fetches the font twice |
|
|
1295
|
+
|
|
1296
|
+
### Inline-CSS-Info-and-Size
|
|
1297
|
+
|
|
1298
|
+
| Code | Severity | When |
|
|
1299
|
+
|------|----------|------|
|
|
1300
|
+
| `over-budget` | warning | Inline CSS above the 14 KB critical budget |
|
|
1301
|
+
| `duplicate-blocks` | warning | Style blocks with identical content |
|
|
1302
|
+
| `large-blocks` | warning | Style blocks above 5 KB |
|
|
1303
|
+
| `styles-in-body` | warning | Style blocks in `<body>` |
|
|
1304
|
+
|
|
1305
|
+
### Webfont-Usage-Analyzer
|
|
1306
|
+
|
|
1307
|
+
`load-failed`, `loaded-unused` and `redeclared` name the font file by its URL in `target`, or the face when it has none.
|
|
1308
|
+
|
|
1309
|
+
| Code | Severity | When |
|
|
1310
|
+
|------|----------|------|
|
|
1311
|
+
| `load-failed` | error | A font that failed to load |
|
|
1312
|
+
| `loaded-unused` | warning | A loaded font that no rendered element uses |
|
|
1313
|
+
| `redeclared` | warning | A loaded font declared again later with the same descriptors, so the page uses the other file |
|
|
1314
|
+
| `blocking-font-display` | warning | Faces with `font-display` `auto` or `block`, which can keep text invisible |
|
|
1315
|
+
| `heavy-fonts` | warning | Web fonts above 300 KB in total |
|
|
1316
|
+
| `never-loaded` | info | Faces declared and never loaded |
|
|
1317
|
+
| `shadowed` | info | Faces declared more than once, whose earlier declarations never loaded |
|
|
1318
|
+
| `unused-subsets` | info | `unicode-range` subsets the page did not need |
|
|
1319
|
+
| `size-unknown` | info | Cross-origin font files without `Timing-Allow-Origin`, so the byte totals are a lower bound |
|
|
1320
|
+
| `unreadable-stylesheets` | info | Cross-origin stylesheets whose font files cannot be matched to faces |
|
|
1321
|
+
| `elements-capped` | info | Only the first 20,000 elements were scanned for text |
|
|
1322
|
+
|
|
1323
|
+
### First-And-Third-Party-Script-Info
|
|
1324
|
+
|
|
1325
|
+
| Code | Severity | When |
|
|
1326
|
+
|------|----------|------|
|
|
1327
|
+
| `unknown-size` | info | Scripts whose size is hidden by a missing `Timing-Allow-Origin`, so the totals are a lower bound |
|
|
1328
|
+
| `render-blocking-unsupported` | info | The browser has no `renderBlockingStatus` (only Chromium has it) |
|
|
1329
|
+
| `third-party-blocking` | error | Third-party scripts that block rendering |
|
|
1330
|
+
| `many-third-party-hosts` | warning | Scripts from more than 3 third-party hosts |
|
|
1331
|
+
| `large-third-party` | warning | Third-party scripts above 100 KB in total |
|
|
1332
|
+
|
|
1333
|
+
### First-And-Third-Party-Script-Timings
|
|
1334
|
+
|
|
1335
|
+
| Code | Severity | When |
|
|
1336
|
+
|------|----------|------|
|
|
1337
|
+
| `slow-scripts` | warning | Scripts that take more than 500 ms to load |
|
|
1338
|
+
| `slow-dns` | warning | A DNS lookup above 100 ms |
|
|
1339
|
+
| `slow-connection` | warning | A TCP connection above 100 ms |
|
|
1340
|
+
| `slow-server` | warning | A server response above 200 ms |
|
|
1341
|
+
|
|
1342
|
+
### Inline-Script-Info-and-Size
|
|
1343
|
+
|
|
1344
|
+
| Code | Severity | When |
|
|
1345
|
+
|------|----------|------|
|
|
1346
|
+
| `parser-blocking-in-head` | error | Inline scripts in `<head>` that block the parser |
|
|
1347
|
+
| `large-inline` | warning | Inline scripts above 2 KB |
|
|
1348
|
+
| `duplicate-scripts` | warning | Inline scripts with identical content |
|
|
1349
|
+
| `analytics-in-head` | warning | Analytics scripts that block in `<head>` |
|
|
1350
|
+
|
|
1351
|
+
### JS-Execution-Time-Breakdown
|
|
1352
|
+
|
|
1353
|
+
| Code | Severity | When |
|
|
1354
|
+
|------|----------|------|
|
|
1355
|
+
| `render-blocking` | error | Scripts that block HTML parsing |
|
|
1356
|
+
| `high-parse-cost` | warning | An estimated parse cost above 500 ms on mobile |
|
|
1357
|
+
| `split-candidates` | warning | Scripts above 50 KB decoded, candidates for code splitting |
|
|
1358
|
+
| `low-compression` | warning | A compression ratio below 2x on more than 10 KB of scripts |
|
|
1359
|
+
|
|
1360
|
+
### SSR-Hydration-Data-Analysis
|
|
1361
|
+
|
|
1362
|
+
| Code | Severity | When |
|
|
1363
|
+
|------|----------|------|
|
|
1364
|
+
| `large-hydration-data` | warning | The hydration data of a framework above its threshold; `target` is the framework |
|
|
1365
|
+
| `large-arrays` | warning | `pageProps` arrays with more than 50 entries |
|
|
1366
|
+
| `deep-nesting` | warning | `pageProps` nested more than 5 levels |
|
|
1367
|
+
| `sensitive-keys` | warning | `pageProps` keys that look sensitive |
|
|
1368
|
+
|
|
1369
|
+
### Third-Party-Impact-by-Domain
|
|
1370
|
+
|
|
1371
|
+
| Code | Severity | When |
|
|
1372
|
+
|------|----------|------|
|
|
1373
|
+
| `render-blocking-domains` | warning | Third-party domains that serve render-blocking resources |
|
|
1374
|
+
| `heavy-domain` | warning | A domain whose scripts take more than 250 ms in long animation frames; `target` is the domain |
|
|
1375
|
+
| `render-blocking-unsupported` | info | The browser has no `renderBlockingStatus` (only Chromium has it) |
|
|
1376
|
+
| `loaf-unattributed` | info | Long animation frames with no third-party script attributed |
|
|
1377
|
+
| `unknown-size` | info | Third-party requests without `Timing-Allow-Origin`, so the transfer size is a lower bound |
|
|
1378
|
+
|
|
1379
|
+
### Validate-Preload-Async-Defer-Scripts
|
|
1380
|
+
|
|
1381
|
+
Each issue names the preloaded script in `target`.
|
|
1382
|
+
|
|
1383
|
+
| Code | Severity | When |
|
|
1384
|
+
|------|----------|------|
|
|
1385
|
+
| `orphan-preload` | warning | A preloaded script that the page neither has nor loaded |
|
|
1386
|
+
| `async-defer-preload` | error | A script with `async` or `defer` that is also preloaded, which raises its priority |
|
|
1387
|
+
| `module-preload` | warning | An ES module preloaded with `rel="preload"` instead of `rel="modulepreload"` |
|
|
1388
|
+
|
|
1389
|
+
### Back-Forward-Cache
|
|
1390
|
+
|
|
1391
|
+
| Code | Severity | When |
|
|
1392
|
+
|------|----------|------|
|
|
1393
|
+
| `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 |
|
|
1394
|
+
| `unload-handler` | error | A `window.onunload` handler |
|
|
1395
|
+
| `cache-control-no-store` | warning | `Cache-Control: no-store` on the page response |
|
|
1396
|
+
| `iframes` | info | Iframes on the page, whose blockers block the whole page |
|
|
1397
|
+
| `service-worker` | info | A Service Worker controls the page, which bfcache allows |
|
|
1398
|
+
|
|
1399
|
+
### Cache-Strategy-Analysis
|
|
1400
|
+
|
|
1401
|
+
Each issue names its resource in `target`, except `header-coverage`. `id` repeats the code, as before the codes.
|
|
1402
|
+
|
|
1403
|
+
| Code | Severity | When |
|
|
1404
|
+
|------|----------|------|
|
|
1405
|
+
| `static-no-store` | error | A static asset with `no-store` |
|
|
1406
|
+
| `versioned-short-cache` | warning | A URL with a hash and a short `max-age` |
|
|
1407
|
+
| `versioned-not-immutable` | info | A URL with a hash and no `immutable` |
|
|
1408
|
+
| `large-no-cache` | error | A large resource with no effective cache |
|
|
1409
|
+
| `expires-without-cc` | warning | An `Expires` header without `Cache-Control` |
|
|
1410
|
+
| `maxage-zero-static` | warning | A static asset with `max-age=0` |
|
|
1411
|
+
| `short-cache-static` | warning | A static asset with a `max-age` under an hour |
|
|
1412
|
+
| `uncompressed` | warning | A text resource sent uncompressed |
|
|
1413
|
+
| `duplicate-uncached` | warning | A resource loaded several times from the network |
|
|
1414
|
+
| `header-coverage` | info | The share of resources whose cache headers could be read |
|
|
1415
|
+
|
|
1416
|
+
### Client-Side-Redirect-Detection
|
|
1417
|
+
|
|
1418
|
+
| Code | Severity | When |
|
|
1419
|
+
|------|----------|------|
|
|
1420
|
+
| `server-redirects` | warning | Server-side redirects before the document |
|
|
1421
|
+
| `document-navigation` | error | Same-origin document navigations, likely a client-side redirect |
|
|
1422
|
+
| `same-origin-navigation` | warning | A same-origin referrer with another path |
|
|
1423
|
+
| `fast-minimal-navigation` | warning | A small same-origin page that loads fast, possibly a redirect page |
|
|
1424
|
+
|
|
1425
|
+
### Compression-Audit
|
|
1426
|
+
|
|
1427
|
+
| Code | Severity | When |
|
|
1428
|
+
|------|----------|------|
|
|
1429
|
+
| `uncompressed` | warning | Text resources served without compression |
|
|
1430
|
+
| `unknown-size` | info | Cross-origin text resources without `Timing-Allow-Origin`, whose compression is unknown |
|
|
1431
|
+
| `content-encoding-unavailable` | info | No `contentEncoding` in Resource Timing, so compression is inferred from the sizes |
|
|
1432
|
+
|
|
1433
|
+
### Prefetch-Resource-Validation
|
|
1434
|
+
|
|
1435
|
+
Each issue about one prefetch names its URL in `target`.
|
|
1436
|
+
|
|
1437
|
+
| Code | Severity | When |
|
|
1438
|
+
|------|----------|------|
|
|
1439
|
+
| `no-prefetch` | info | No `rel="prefetch"` hints on the page |
|
|
1440
|
+
| `missing-as` | warning | A prefetch without `as` |
|
|
1441
|
+
| `invalid-as` | warning | A prefetch with an `as` value that does not exist |
|
|
1442
|
+
| `duplicate-prefetch` | warning | A URL prefetched more than once |
|
|
1443
|
+
| `used-on-current-page` | error | A prefetched resource that the current page uses, which needs `preload` |
|
|
1444
|
+
| `large-file` | warning | A prefetched file above 500 KB |
|
|
1445
|
+
| `inappropriate-type` | warning | A prefetched type that may not suit prefetch |
|
|
1446
|
+
| `too-many-prefetches` | warning | More than 10 prefetch hints |
|
|
1447
|
+
| `large-total-size` | warning or error | Prefetches above 2 MB in total, an error above 5 MB |
|
|
1448
|
+
|
|
1449
|
+
### Priority-Hints-Audit
|
|
1450
|
+
|
|
1451
|
+
Each issue names its resource in `target`.
|
|
1452
|
+
|
|
1453
|
+
| Code | Severity | When |
|
|
1454
|
+
|------|----------|------|
|
|
1455
|
+
| `preload-low-priority` | error | A preload with `fetchpriority="low"` |
|
|
1456
|
+
| `low-priority-loaded-early` | warning | A `fetchpriority="low"` resource that still loaded early |
|
|
1457
|
+
| `high-priority-loaded-late` | warning | A `fetchpriority="high"` resource that still loaded late |
|
|
1458
|
+
| `lcp-no-fetchpriority` | warning | The LCP candidate image without `fetchpriority="high"` |
|
|
1459
|
+
|
|
1460
|
+
### Resource-Hints
|
|
1461
|
+
|
|
1462
|
+
Each issue about one hint names its URL in `target`.
|
|
1463
|
+
|
|
1464
|
+
| Code | Severity | When |
|
|
1465
|
+
|------|----------|------|
|
|
1466
|
+
| `preload-missing-as` | error | A preload without `as` |
|
|
1467
|
+
| `font-preload-no-crossorigin` | error | A font preload without `crossorigin`, which fetches the font twice |
|
|
1468
|
+
| `preconnect-no-crossorigin` | warning | A preconnect without `crossorigin` to an origin that serves CORS resources |
|
|
1469
|
+
| `unused-preload` | warning | A preload that the page did not use during the load |
|
|
1470
|
+
| `unused-preconnect` | warning | A preconnect to an origin the page loaded nothing from |
|
|
1471
|
+
| `too-many-preloads` | warning | More than 6 preloads |
|
|
1472
|
+
|
|
1473
|
+
### Resource-Hints-Validation
|
|
1474
|
+
|
|
1475
|
+
| Code | Severity | When |
|
|
1476
|
+
|------|----------|------|
|
|
1477
|
+
| `unused-hints` | warning | Hints the page did not use |
|
|
1478
|
+
| `missing-connection-hints` | info | Domains that could use a connection hint |
|
|
1479
|
+
| `redundant-hints` | warning | Domains with both `preconnect` and `dns-prefetch` |
|
|
1480
|
+
|
|
1481
|
+
### Server-Timing-Early-Hints
|
|
1482
|
+
|
|
1483
|
+
| Code | Severity | When |
|
|
1484
|
+
|------|----------|------|
|
|
1485
|
+
| `no-server-timing` | info | The document response has no `Server-Timing` header |
|
|
1486
|
+
| `timing-hidden` | info | Cross-origin resources without `Timing-Allow-Origin`, whose Server-Timing cannot be read |
|
|
1487
|
+
| `no-early-hints` | info | No 103 Early Hints response for the document |
|
|
1488
|
+
|
|
1489
|
+
### Service-Worker-Analysis
|
|
1490
|
+
|
|
1491
|
+
| Code | Severity | When |
|
|
1492
|
+
|------|----------|------|
|
|
1493
|
+
| `unsupported` | error | The browser has no Service Workers |
|
|
1494
|
+
| `no-service-worker` | info | No Service Worker registered |
|
|
1495
|
+
| `not-controlled` | warning | The page is not controlled by its Service Worker, after a hard reload or on a first visit |
|
|
1496
|
+
| `update-pending` | warning | A waiting update; `target` is the scope |
|
|
1497
|
+
| `navigation-preload-disabled` | info | Navigation Preload is off; `target` is the scope |
|
|
1498
|
+
| `slow-startup` | warning or error | A Service Worker startup above 50 ms, an error above 100 ms |
|
|
1499
|
+
| `low-cache-hit-rate` | warning or error | A cache hit rate under 80%, an error under 50% |
|
|
1500
|
+
| `unknown-source` | info | Intercepted cross-origin resources without `Timing-Allow-Origin`, left out of the hit rate |
|
|
1501
|
+
|
|
1502
|
+
### Speculation-Rules-Inspector
|
|
1503
|
+
|
|
1504
|
+
| Code | Severity | When |
|
|
1505
|
+
|------|----------|------|
|
|
1506
|
+
| `invalid-json` | error | A speculation rules script that is not valid JSON |
|
|
1507
|
+
| `eager-list-capped` | warning | An eager prerender list with more than 10 URLs, above what Chrome prerenders |
|
|
1508
|
+
| `eager-document-rule` | warning | A document prerender rule with eager eagerness |
|
|
1509
|
+
| `no-rules` | info | No speculation rules in the DOM |
|
|
1510
|
+
| `no-prefetch-or-prerender` | info | Speculation rules scripts without prefetch or prerender rules |
|
|
1511
|
+
| `unsupported` | info | The browser does not support speculation rules |
|
|
1512
|
+
| `prerendered` | info | The page was prerendered, so its timings start at `activationStart` |
|
|
1513
|
+
|
|
1514
|
+
### TTFB-Resources
|
|
1515
|
+
|
|
1516
|
+
| Code | Severity | When |
|
|
1517
|
+
|------|----------|------|
|
|
1518
|
+
| `no-resources` | info | No resources with TTFB data |
|
|
1519
|
+
|
|
1520
|
+
### TTFB-Sub-Parts
|
|
1521
|
+
|
|
1522
|
+
| Code | Severity | When |
|
|
1523
|
+
|------|----------|------|
|
|
1524
|
+
| `slow-ttfb` | warning or error | A TTFB that is not good, an error when poor, naming its slowest phase |
|
|
1525
|
+
|
|
1526
|
+
### DOM-Size-and-Depth
|
|
1527
|
+
|
|
1528
|
+
| Code | Severity | When |
|
|
1529
|
+
|------|----------|------|
|
|
1530
|
+
| `too-many-nodes` | warning or error | More than 800 nodes, an error above 1,400 |
|
|
1531
|
+
| `too-deep` | warning | A DOM deeper than 32 levels |
|
|
1532
|
+
| `too-many-children` | warning | An element with more than 60 children |
|
|
1533
|
+
|
|
1534
|
+
### Forced-Synchronous-Layout
|
|
1535
|
+
|
|
1536
|
+
| Code | Severity | When |
|
|
1537
|
+
|------|----------|------|
|
|
1538
|
+
| `forced-layout` | warning or error | A layout property read after a write forced a layout, an error above 5 times; `target` is the property |
|
|
1539
|
+
|
|
1540
|
+
### Input-Latency-Breakdown
|
|
1541
|
+
|
|
1542
|
+
| Code | Severity | When |
|
|
1543
|
+
|------|----------|------|
|
|
1544
|
+
| `slow-event-type` | warning or error | An event type whose P75 is not good, an error when poor; `target` is the event type |
|
|
1545
|
+
|
|
1546
|
+
### Interactions
|
|
1547
|
+
|
|
1548
|
+
| Code | Severity | When |
|
|
1549
|
+
|------|----------|------|
|
|
1550
|
+
| `slow-interaction` | warning or error | An interaction above 200 ms, an error above 500 ms; `target` is its type |
|
|
1551
|
+
|
|
1552
|
+
### Scroll-Performance
|
|
1553
|
+
|
|
1554
|
+
| Code | Severity | When |
|
|
1555
|
+
|------|----------|------|
|
|
1556
|
+
| `non-passive-listeners` | warning | Scroll or touch listeners without `{ passive: true }` |
|
|
1557
|
+
| `will-change` | info | Elements with `will-change` set |
|
|
1558
|
+
|
|
1559
|
+
### Image-Element-Audit
|
|
1560
|
+
|
|
1561
|
+
Each issue names its image in `target`, except `lcp-not-img`.
|
|
1562
|
+
|
|
1563
|
+
| Code | Severity | When |
|
|
1564
|
+
|------|----------|------|
|
|
1565
|
+
| `lcp-not-img` | info | The LCP element is not an `<img>` |
|
|
1566
|
+
| `lcp-no-fetchpriority` | error | The LCP image has no `fetchpriority="high"` |
|
|
1567
|
+
| `lcp-lazy` | error | The LCP image has `loading="lazy"` |
|
|
1568
|
+
| `lcp-not-decoding-sync` | warning | The LCP image has no `decoding="sync"` |
|
|
1569
|
+
| `lcp-not-preloaded` | warning | The LCP image has no `<link rel="preload" as="image">` |
|
|
1570
|
+
| `lcp-preload-no-fetchpriority` | info | The preload of the LCP image has no `fetchpriority="high"` |
|
|
1571
|
+
| `lazy-in-viewport` | warning | An image in the viewport with `loading="lazy"` |
|
|
1572
|
+
| `eager-off-viewport` | warning | An image outside the viewport without `loading="lazy"` |
|
|
1573
|
+
| `high-priority-off-viewport` | error | An image outside the viewport with `fetchpriority="high"` |
|
|
1574
|
+
| `lazy-and-high-priority` | error | An image with both `loading="lazy"` and `fetchpriority="high"` |
|
|
1575
|
+
| `missing-dimensions` | warning | An image without `width` and `height` |
|
|
1576
|
+
| `no-modern-format` | info | An image that is not WebP, AVIF, JPEG XL or SVG, outside a `<picture>` |
|
|
1577
|
+
|
|
1578
|
+
### Oversized-Images
|
|
1579
|
+
|
|
1580
|
+
Each issue names its image in `target`.
|
|
1581
|
+
|
|
1582
|
+
| Code | Severity | When |
|
|
1583
|
+
|------|----------|------|
|
|
1584
|
+
| `oversized` | info or warning | A file larger than its slot needs, a warning when it is far larger |
|
|
1585
|
+
| `undersized` | info or warning | A file smaller than its slot needs, so it looks blurry, a warning when it is far smaller |
|
|
1586
|
+
|
|
1587
|
+
### SVG-Embedded-Bitmap-Analysis
|
|
1588
|
+
|
|
1589
|
+
| Code | Severity | When |
|
|
1590
|
+
|------|----------|------|
|
|
1591
|
+
| `embedded-bitmaps` | warning | SVG files that contain bitmaps |
|
|
1592
|
+
| `sprite-opportunity` | info | Standalone inline SVGs that could share one `<symbol>` sprite |
|
|
1593
|
+
|
|
1594
|
+
### Video-Element-Audit
|
|
1595
|
+
|
|
1596
|
+
Each issue names its video in `target`, except `no-videos`.
|
|
1597
|
+
|
|
1598
|
+
| Code | Severity | When |
|
|
1599
|
+
|------|----------|------|
|
|
1600
|
+
| `no-videos` | info | No `<video>` elements on the page |
|
|
1601
|
+
| `preload-auto` | error | `preload="auto"` on a video without `autoplay` |
|
|
1602
|
+
| `off-viewport-preload` | warning | A video outside the viewport without `preload="none"` |
|
|
1603
|
+
| `autoplay-not-muted` | error | `autoplay` without `muted` |
|
|
1604
|
+
| `autoplay-no-playsinline` | warning | `autoplay` without `playsinline` |
|
|
1605
|
+
| `missing-poster` | warning | A video without a poster |
|
|
1606
|
+
| `missing-dimensions` | warning | A video without `width` and `height` |
|
|
1607
|
+
| `no-modern-codec` | info | No WebM source (VP9 or AV1) |
|
|
1608
|
+
| `no-controls` | info | A video without `controls` and without `autoplay` |
|
|
1609
|
+
|
|
1610
|
+
---
|
|
1611
|
+
|
|
1192
1612
|
## Guidelines for Agents
|
|
1193
1613
|
|
|
1194
1614
|
### Reading results
|
|
@@ -1223,6 +1643,7 @@ data = evaluate_script("getINP()")
|
|
|
1223
1643
|
- `rating === "poor"` → high priority fix, check `issues` for specific problems
|
|
1224
1644
|
- `count > 0` and `issues.length > 0` → audit found actionable problems
|
|
1225
1645
|
- `count === 0` → nothing to audit (no render-blocking resources, no images, etc.)
|
|
1646
|
+
- To tell whether an issue is new, fixed or the same as in an earlier run, compare `script`, `code` and `target`, not `message`
|
|
1226
1647
|
|
|
1227
1648
|
---
|
|
1228
1649
|
|
|
@@ -1240,5 +1661,6 @@ data = evaluate_script("getINP()")
|
|
|
1240
1661
|
10. **Valid statuses only** — `ok`, `tracking`, `error` or `unsupported`, with an `error` message when the status is `error` or `unsupported`.
|
|
1241
1662
|
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
1663
|
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.
|
|
1664
|
+
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
1665
|
|
|
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.
|
|
1666
|
+
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.
|
package/src/baseline.js
CHANGED
|
@@ -116,14 +116,38 @@ function compareMetrics(base, current, thresholds) {
|
|
|
116
116
|
return metrics;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
119
|
+
// A resource of the page itself by its path and query, so the review app of a PR, on another host,
|
|
120
|
+
// matches main; any other target as it is
|
|
121
|
+
function targetOf(target, pageUrl) {
|
|
122
|
+
if (!target) return "";
|
|
123
|
+
try {
|
|
124
|
+
const url = new URL(target);
|
|
125
|
+
return url.origin === new URL(pageUrl).origin ? url.pathname + url.search : url.href;
|
|
126
|
+
} catch {
|
|
127
|
+
return target;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Two errors are the same when they share a code and a target or, when a snippet has no codes on one
|
|
132
|
+
// side (a baseline of 1.4.0), when their messages differ only in their figures: "Render-blocking CSS
|
|
133
|
+
// (69.21 KB)…" and "Render-blocking CSS (71.80 KB)…"
|
|
134
|
+
const byMessage = (error) => `${error.id}\u0000${error.message.replace(/\d+(?:[.,]\d+)*/g, "#")}`;
|
|
122
135
|
|
|
123
136
|
function compareErrors(base, current) {
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
137
|
+
const baseErrors = blockingErrors(base.results ?? []);
|
|
138
|
+
const currentErrors = blockingErrors(current.results ?? []);
|
|
139
|
+
// The snippets whose errors on one side all lack a code pair by message on both
|
|
140
|
+
const uncoded = (errors) => {
|
|
141
|
+
const ids = new Set(errors.map((e) => e.id));
|
|
142
|
+
for (const e of errors) if (e.code) ids.delete(e.id);
|
|
143
|
+
return ids;
|
|
144
|
+
};
|
|
145
|
+
const byMessageOnly = new Set([...uncoded(baseErrors), ...uncoded(currentErrors)]);
|
|
146
|
+
const keyOf = (error, pageUrl) =>
|
|
147
|
+
error.code && !byMessageOnly.has(error.id) ? `${error.id}\u0000${error.code}\u0000${targetOf(error.target, pageUrl)}` : byMessage(error);
|
|
148
|
+
const byKey = (errors, pageUrl) => new Map(errors.map((error) => [keyOf(error, pageUrl), error]));
|
|
149
|
+
const b = byKey(baseErrors, base.url);
|
|
150
|
+
const c = byKey(currentErrors, current.url);
|
|
127
151
|
return {
|
|
128
152
|
new: [...c].filter(([key]) => !b.has(key)).map(([, error]) => error),
|
|
129
153
|
fixed: [...b].filter(([key]) => !c.has(key)).map(([, error]) => error),
|
package/src/blocking.js
CHANGED
|
@@ -7,7 +7,7 @@ export function blockingErrors(results) {
|
|
|
7
7
|
if (r.status === "error") errors.push({ id: r.id, message: r.error });
|
|
8
8
|
if (r.reason || !Array.isArray(r.issues)) continue;
|
|
9
9
|
for (const issue of r.issues) {
|
|
10
|
-
if (issue.severity === "error") errors.push({ id: r.id, message: issue.message, ...(issue.code ? { code: issue.code } : {}) });
|
|
10
|
+
if (issue.severity === "error") errors.push({ id: r.id, message: issue.message, ...(issue.code ? { code: issue.code } : {}), ...(issue.target ? { target: issue.target } : {}) });
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
return errors;
|
package/src/mcp/documents.js
CHANGED
|
@@ -19,30 +19,50 @@ function readDocument(relativePath) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
// Splits SCHEMA.md into the general contract and the section of each snippet ("#### LCP" up to the next
|
|
22
|
-
// heading of that level or above). The general contract is everything outside "## Script-Specific Schemas"
|
|
22
|
+
// heading of that level or above). The general contract is everything outside "## Script-Specific Schemas"
|
|
23
|
+
// and outside the tables of "## Issue codes", whose rules it keeps. The table of a snippet ("### LCP"
|
|
24
|
+
// under Issue codes) goes with its section, or makes one when the snippet has none.
|
|
23
25
|
export function splitSchema(markdown) {
|
|
24
26
|
const lines = markdown.split("\n");
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const sectionOf = (heading) => {
|
|
28
|
+
const start = lines.findIndex((l) => l.startsWith(heading));
|
|
29
|
+
if (start === -1) return [lines.length, lines.length];
|
|
30
|
+
const next = lines.findIndex((l, i) => i > start && l.startsWith("## "));
|
|
31
|
+
return [start, next === -1 ? lines.length : next];
|
|
32
|
+
};
|
|
33
|
+
const [schemasStart, schemasEnd] = sectionOf("## Script-Specific Schemas");
|
|
34
|
+
const [codesStart, codesEnd] = sectionOf("## Issue codes");
|
|
35
|
+
const firstTable = lines.findIndex((l, i) => i > codesStart && i < codesEnd && l.startsWith("### "));
|
|
36
|
+
const tablesStart = firstTable === -1 ? codesEnd : firstTable;
|
|
37
|
+
const general = lines
|
|
38
|
+
.filter((_, i) => !(i >= schemasStart && i < schemasEnd) && !(i >= tablesStart && i < codesEnd))
|
|
39
|
+
.join("\n");
|
|
28
40
|
|
|
29
41
|
const sections = new Map();
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
const collect = (from, to, level, add) => {
|
|
43
|
+
let name = null;
|
|
44
|
+
let current = [];
|
|
45
|
+
const flush = () => name && add(name, current.join("\n").trim());
|
|
46
|
+
for (const line of lines.slice(from, to)) {
|
|
47
|
+
if (line.startsWith(`${level} `)) {
|
|
48
|
+
flush();
|
|
49
|
+
name = line.slice(level.length + 1).split(" ")[0];
|
|
50
|
+
current = [line];
|
|
51
|
+
} else if (/^#{1,4} /.test(line)) {
|
|
52
|
+
flush();
|
|
53
|
+
name = null;
|
|
54
|
+
} else if (name) {
|
|
55
|
+
current.push(line);
|
|
56
|
+
}
|
|
43
57
|
}
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
flush();
|
|
59
|
+
};
|
|
60
|
+
collect(schemasStart, schemasEnd, "####", (name, text) => sections.set(name, text));
|
|
61
|
+
collect(tablesStart, codesEnd, "###", (name, text) => {
|
|
62
|
+
// The last table runs into the rule that closes the section
|
|
63
|
+
const table = `Issue codes:\n${text.split("\n").slice(1).join("\n").replace(/\n+---$/, "")}`;
|
|
64
|
+
sections.set(name, sections.has(name) ? `${sections.get(name)}\n\n${table}` : `#### ${name}\n\n${table}`);
|
|
65
|
+
});
|
|
46
66
|
return { general, sections };
|
|
47
67
|
}
|
|
48
68
|
|
|
@@ -65,7 +85,7 @@ export function registerResources(server) {
|
|
|
65
85
|
"webperf://schema",
|
|
66
86
|
{
|
|
67
87
|
title: "Snippet result contract",
|
|
68
|
-
description: "What every snippet returns: the base shape, the statuses, the execution patterns
|
|
88
|
+
description: "What every snippet returns: the base shape, the statuses, the execution patterns, how to read a result and the rules of the issue codes. The fields and the issue codes of each snippet are in webperf://schema/{snippet}.",
|
|
69
89
|
mimeType: MARKDOWN,
|
|
70
90
|
},
|
|
71
91
|
async (uri) => markdown(uri, schemaSection())
|
|
@@ -80,7 +100,7 @@ export function registerResources(server) {
|
|
|
80
100
|
}),
|
|
81
101
|
{
|
|
82
102
|
title: "Result of one snippet",
|
|
83
|
-
description: "The fields one snippet returns, with an example, from snippets/SCHEMA.md.",
|
|
103
|
+
description: "The fields one snippet returns, with an example, and the codes of its issues, from snippets/SCHEMA.md.",
|
|
84
104
|
mimeType: MARKDOWN,
|
|
85
105
|
},
|
|
86
106
|
async (uri, { snippet }) => {
|