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
|
@@ -146,6 +146,7 @@
|
|
|
146
146
|
|
|
147
147
|
if (totalSize > criticalBudget) {
|
|
148
148
|
issues.push({
|
|
149
|
+
code: "over-budget",
|
|
149
150
|
type: "size",
|
|
150
151
|
message: `Total inline CSS (${formatBytes(totalSize)}) exceeds 14 KB critical budget`,
|
|
151
152
|
suggestion: "Move non-critical styles to external stylesheet",
|
|
@@ -154,6 +155,7 @@
|
|
|
154
155
|
|
|
155
156
|
if (duplicates.length > 0) {
|
|
156
157
|
issues.push({
|
|
158
|
+
code: "duplicate-blocks",
|
|
157
159
|
type: "duplicate",
|
|
158
160
|
message: `Found ${duplicates.length} duplicated style block(s)`,
|
|
159
161
|
details: duplicates.map((d) => `Tags #${d.join(", #")} have identical content`),
|
|
@@ -164,6 +166,7 @@
|
|
|
164
166
|
const largeBlocks = styles.filter((s) => s.size > 5 * 1024);
|
|
165
167
|
if (largeBlocks.length > 0) {
|
|
166
168
|
issues.push({
|
|
169
|
+
code: "large-blocks",
|
|
167
170
|
type: "large",
|
|
168
171
|
message: `${largeBlocks.length} style block(s) over 5 KB`,
|
|
169
172
|
details: largeBlocks.map((s) => `Tag #${s.index}: ${formatBytes(s.size)}`),
|
|
@@ -174,6 +177,7 @@
|
|
|
174
177
|
const bodyStyles = styles.filter((s) => !s.inHead);
|
|
175
178
|
if (bodyStyles.length > 0) {
|
|
176
179
|
issues.push({
|
|
180
|
+
code: "styles-in-body",
|
|
177
181
|
type: "location",
|
|
178
182
|
message: `${bodyStyles.length} style block(s) in <body>`,
|
|
179
183
|
details: bodyStyles.map((s) => `Tag #${s.index}`),
|
|
@@ -231,6 +235,6 @@
|
|
|
231
235
|
bodyStyleBlocks: styles.filter(s => !s.inHead).length,
|
|
232
236
|
},
|
|
233
237
|
items: styles.map(s => ({ index: s.index, sizeBytes: s.size, parent: s.parent, inHead: s.inHead, ruleCount: s.ruleCount, hasMediaQueries: s.hasMediaQueries, hasKeyframes: s.hasKeyframes })),
|
|
234
|
-
issues: issues.map(i => ({ severity: "warning", message: i.message })),
|
|
238
|
+
issues: issues.map(i => ({ code: i.code, severity: "warning", message: i.message })),
|
|
235
239
|
};
|
|
236
240
|
})();
|
|
@@ -239,6 +239,7 @@
|
|
|
239
239
|
|
|
240
240
|
if (parserBlockingInHead.length > 0) {
|
|
241
241
|
issues.push({
|
|
242
|
+
code: "parser-blocking-in-head",
|
|
242
243
|
type: "blocking",
|
|
243
244
|
message: `${parserBlockingInHead.length} parser-blocking script(s) in <head>`,
|
|
244
245
|
details: parserBlockingInHead.map(
|
|
@@ -251,6 +252,7 @@
|
|
|
251
252
|
const largeScripts = executableScripts.filter((s) => s.size > 2 * 1024);
|
|
252
253
|
if (largeScripts.length > 0) {
|
|
253
254
|
issues.push({
|
|
255
|
+
code: "large-inline",
|
|
254
256
|
type: "large",
|
|
255
257
|
message: `${largeScripts.length} script(s) over 2 KB`,
|
|
256
258
|
details: largeScripts.map((s) => `Script #${s.index}: ${formatBytes(s.size)}`),
|
|
@@ -260,6 +262,7 @@
|
|
|
260
262
|
|
|
261
263
|
if (duplicates.length > 0) {
|
|
262
264
|
issues.push({
|
|
265
|
+
code: "duplicate-scripts",
|
|
263
266
|
type: "duplicate",
|
|
264
267
|
message: `${duplicates.length} duplicated script(s) detected`,
|
|
265
268
|
details: duplicates.map((d) => `Scripts #${d.join(", #")} have identical content`),
|
|
@@ -272,6 +275,7 @@
|
|
|
272
275
|
);
|
|
273
276
|
if (analyticsInHead.length > 0) {
|
|
274
277
|
issues.push({
|
|
278
|
+
code: "analytics-in-head",
|
|
275
279
|
type: "analytics",
|
|
276
280
|
message: `${analyticsInHead.length} analytics script(s) blocking in <head>`,
|
|
277
281
|
details: analyticsInHead.map((s) => `Script #${s.index}`),
|
|
@@ -345,6 +349,6 @@
|
|
|
345
349
|
dataPayloadCount: scripts.filter((s) => s.isDataPayload).length,
|
|
346
350
|
},
|
|
347
351
|
items: rankedScripts.slice(0, MAX_ITEMS).map(s => ({ index: s.index, sizeBytes: s.size, category: s.category, inHead: s.inHead, isParserBlocking: s.isParserBlocking, isAnalytics: s.isAnalytics, isConfig: s.isConfig, isDataPayload: s.isDataPayload })),
|
|
348
|
-
issues: issues.map(i => ({ severity: i.type === "blocking" ? "error" : "warning", message: i.message })),
|
|
352
|
+
issues: issues.map(i => ({ code: i.code, severity: i.type === "blocking" ? "error" : "warning", message: i.message })),
|
|
349
353
|
};
|
|
350
354
|
})();
|
|
@@ -348,10 +348,10 @@
|
|
|
348
348
|
},
|
|
349
349
|
items: scripts.slice(0, 50).map(s => ({ shortName: s.shortName, isBlocking: s.isBlocking, downloadMs: Math.round(s.downloadDuration), estimatedParseMobileMs: s.estimatedParseMobile, transferBytes: s.transferSize, decodedBytes: s.decodedSize, corsRestricted: s.corsRestricted })),
|
|
350
350
|
issues: [
|
|
351
|
-
...(blocking.length > 0 ? [{ severity: "error", message: `${blocking.length} render-blocking script(s) delay HTML parsing` }] : []),
|
|
352
|
-
...(hasHighParseTime ? [{ severity: "warning", message: `High estimated parse cost (~${Math.round(totalEstParse)}ms on mobile)` }] : []),
|
|
353
|
-
...(splitCandidates.length > 0 ? [{ severity: "warning", message: `${splitCandidates.length} script(s) over 50KB decoded. Consider code splitting.` }] : []),
|
|
354
|
-
...(poorCompression ? [{ severity: "warning", message: `Low compression ratio (${compressionRatio.toFixed(1)}x). Enable Brotli or gzip.` }] : []),
|
|
351
|
+
...(blocking.length > 0 ? [{ code: "render-blocking", severity: "error", message: `${blocking.length} render-blocking script(s) delay HTML parsing` }] : []),
|
|
352
|
+
...(hasHighParseTime ? [{ code: "high-parse-cost", severity: "warning", message: `High estimated parse cost (~${Math.round(totalEstParse)}ms on mobile)` }] : []),
|
|
353
|
+
...(splitCandidates.length > 0 ? [{ code: "split-candidates", severity: "warning", message: `${splitCandidates.length} script(s) over 50KB decoded. Consider code splitting.` }] : []),
|
|
354
|
+
...(poorCompression ? [{ code: "low-compression", severity: "warning", message: `Low compression ratio (${compressionRatio.toFixed(1)}x). Enable Brotli or gzip.` }] : []),
|
|
355
355
|
],
|
|
356
356
|
};
|
|
357
357
|
})();
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
status: "ok",
|
|
29
29
|
count: 0,
|
|
30
30
|
items: [],
|
|
31
|
-
issues: [{ severity: "info", message: 'No prefetch hints on this page (rel="prefetch"), so there was nothing to validate.' }],
|
|
31
|
+
issues: [{ code: "no-prefetch", severity: "info", message: 'No prefetch hints on this page (rel="prefetch"), so there was nothing to validate.' }],
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -116,12 +116,14 @@
|
|
|
116
116
|
// Check for missing or invalid 'as' attribute
|
|
117
117
|
if (as === "unknown") {
|
|
118
118
|
analysis.warnings.push({
|
|
119
|
+
code: "missing-as",
|
|
119
120
|
type: "missing-as",
|
|
120
121
|
severity: "warning",
|
|
121
122
|
message: "Missing 'as' attribute - browser cannot apply correct MIME type matching",
|
|
122
123
|
});
|
|
123
124
|
} else if (!validAsValues.has(as)) {
|
|
124
125
|
analysis.warnings.push({
|
|
126
|
+
code: "invalid-as",
|
|
125
127
|
type: "invalid-as",
|
|
126
128
|
severity: "warning",
|
|
127
129
|
message: `Invalid 'as' value: "${as}" - should be one of: ${Array.from(validAsValues).join(", ")}`,
|
|
@@ -131,6 +133,7 @@
|
|
|
131
133
|
// Check for duplicate prefetch
|
|
132
134
|
if (seenUrls.has(normalizedUrl)) {
|
|
133
135
|
analysis.warnings.push({
|
|
136
|
+
code: "duplicate-prefetch",
|
|
134
137
|
type: "duplicate-prefetch",
|
|
135
138
|
severity: "warning",
|
|
136
139
|
message: "Duplicate prefetch - this URL is prefetched multiple times",
|
|
@@ -169,6 +172,7 @@
|
|
|
169
172
|
if (isCurrentPageResource) {
|
|
170
173
|
analysis.isCurrentPage = true;
|
|
171
174
|
analysis.warnings.push({
|
|
175
|
+
code: "used-on-current-page",
|
|
172
176
|
type: "wrong-hint",
|
|
173
177
|
severity: "error",
|
|
174
178
|
message: "Resource used on current page - should use preload instead",
|
|
@@ -179,6 +183,7 @@
|
|
|
179
183
|
if (analysis.size > THRESHOLDS.largeFileSize) {
|
|
180
184
|
const sizeMB = (analysis.size / (1024 * 1024)).toFixed(2);
|
|
181
185
|
analysis.warnings.push({
|
|
186
|
+
code: "large-file",
|
|
182
187
|
type: "large-file",
|
|
183
188
|
severity: "warning",
|
|
184
189
|
message: `Large file (${sizeMB}MB) - consider if prefetch is appropriate`,
|
|
@@ -203,6 +208,7 @@
|
|
|
203
208
|
|
|
204
209
|
if (isInappropriate) {
|
|
205
210
|
analysis.warnings.push({
|
|
211
|
+
code: "inappropriate-type",
|
|
206
212
|
type: "inappropriate-type",
|
|
207
213
|
severity: "warning",
|
|
208
214
|
message: `Type "${as}" may not be suitable for prefetch: ${inappropriateReason}`,
|
|
@@ -229,6 +235,7 @@
|
|
|
229
235
|
const countIssues = [];
|
|
230
236
|
if (prefetchLinks.length > THRESHOLDS.maxCount) {
|
|
231
237
|
countIssues.push({
|
|
238
|
+
code: "too-many-prefetches",
|
|
232
239
|
type: "excessive-count",
|
|
233
240
|
severity: "warning",
|
|
234
241
|
message: `${prefetchLinks.length} prefetch hints found (recommended: <${THRESHOLDS.maxCount})`,
|
|
@@ -242,6 +249,7 @@
|
|
|
242
249
|
|
|
243
250
|
if (totalSize > THRESHOLDS.totalSizeCritical) {
|
|
244
251
|
countIssues.push({
|
|
252
|
+
code: "large-total-size",
|
|
245
253
|
type: "excessive-size",
|
|
246
254
|
severity: "error",
|
|
247
255
|
message: `Total prefetch size: ${totalSizeMB}MB (critical threshold exceeded)`,
|
|
@@ -249,6 +257,7 @@
|
|
|
249
257
|
});
|
|
250
258
|
} else if (totalSize > THRESHOLDS.totalSizeWarning) {
|
|
251
259
|
countIssues.push({
|
|
260
|
+
code: "large-total-size",
|
|
252
261
|
type: "high-size",
|
|
253
262
|
severity: "warning",
|
|
254
263
|
message: `Total prefetch size: ${totalSizeMB}MB (warning threshold exceeded)`,
|
|
@@ -463,8 +472,8 @@
|
|
|
463
472
|
},
|
|
464
473
|
items: [...validPrefetch, ...issues].map(item => ({ href: item.shortUrl, as: item.as, sizeBytes: item.size, loaded: item.loaded, isCurrentPage: item.isCurrentPage || false })),
|
|
465
474
|
issues: [
|
|
466
|
-
...countIssues.map(i => ({ severity: i.severity === "error" ? "error" : "warning", message: i.message })),
|
|
467
|
-
...issues.flatMap(item => item.warnings.filter(w => w.severity !== "info").map(w => ({ severity: w.severity, message: `${item.shortUrl}: ${w.message}
|
|
475
|
+
...countIssues.map(i => ({ code: i.code, severity: i.severity === "error" ? "error" : "warning", message: i.message })),
|
|
476
|
+
...issues.flatMap(item => item.warnings.filter(w => w.severity !== "info").map(w => ({ code: w.code, severity: w.severity, message: `${item.shortUrl}: ${w.message}`, target: item.href }))),
|
|
468
477
|
],
|
|
469
478
|
};
|
|
470
479
|
})();
|
|
@@ -77,8 +77,10 @@
|
|
|
77
77
|
.filter((link) => link.getAttribute("fetchpriority") === "low")
|
|
78
78
|
.forEach((link) => {
|
|
79
79
|
issues.push({
|
|
80
|
+
code: "preload-low-priority",
|
|
80
81
|
severity: "error",
|
|
81
82
|
element: link,
|
|
83
|
+
url: link.href,
|
|
82
84
|
resource: shortUrl(link),
|
|
83
85
|
message: 'preload + fetchpriority="low" is contradictory',
|
|
84
86
|
detail:
|
|
@@ -94,8 +96,10 @@
|
|
|
94
96
|
const entry = getEntry(el);
|
|
95
97
|
if (entry && entry.startTime < 500) {
|
|
96
98
|
issues.push({
|
|
99
|
+
code: "low-priority-loaded-early",
|
|
97
100
|
severity: "warning",
|
|
98
101
|
element: el,
|
|
102
|
+
url: el.src || el.href,
|
|
99
103
|
resource: shortUrl(el),
|
|
100
104
|
message: `fetchpriority="low" but loaded at ${entry.startTime.toFixed(0)}ms`,
|
|
101
105
|
detail:
|
|
@@ -112,8 +116,10 @@
|
|
|
112
116
|
const entry = getEntry(el);
|
|
113
117
|
if (entry && entry.startTime > 3000) {
|
|
114
118
|
issues.push({
|
|
119
|
+
code: "high-priority-loaded-late",
|
|
115
120
|
severity: "warning",
|
|
116
121
|
element: el,
|
|
122
|
+
url: el.src || el.href,
|
|
117
123
|
resource: shortUrl(el),
|
|
118
124
|
message: `fetchpriority="high" but loaded at ${entry.startTime.toFixed(0)}ms`,
|
|
119
125
|
detail:
|
|
@@ -127,8 +133,10 @@
|
|
|
127
133
|
if (lcpCandidate && lcpCandidate.getAttribute("fetchpriority") !== "high") {
|
|
128
134
|
const fp = lcpCandidate.getAttribute("fetchpriority");
|
|
129
135
|
issues.push({
|
|
136
|
+
code: "lcp-no-fetchpriority",
|
|
130
137
|
severity: "warning",
|
|
131
138
|
element: lcpCandidate,
|
|
139
|
+
url: lcpCandidate.currentSrc || lcpCandidate.src,
|
|
132
140
|
resource:
|
|
133
141
|
(lcpCandidate.currentSrc || lcpCandidate.src).split("/").pop()?.split("?")[0] ||
|
|
134
142
|
"LCP image",
|
|
@@ -340,8 +348,10 @@
|
|
|
340
348
|
resource: (el.src || el.href || el.getAttribute("href") || "").split("/").pop()?.split("?")[0] || "",
|
|
341
349
|
})),
|
|
342
350
|
issues: issues.map(i => ({
|
|
351
|
+
code: i.code,
|
|
343
352
|
severity: i.severity,
|
|
344
353
|
message: i.fix ? `${i.message.replace(/\.$/, "")}. Fix: ${i.fix}` : i.message,
|
|
354
|
+
...(i.url ? { target: i.url } : {}),
|
|
345
355
|
})),
|
|
346
356
|
};
|
|
347
357
|
})();
|
|
@@ -538,9 +538,9 @@
|
|
|
538
538
|
})),
|
|
539
539
|
].slice(0, 50),
|
|
540
540
|
issues: [
|
|
541
|
-
...(totalIssues > 0 ? [{ severity: "warning", message: `${totalIssues} unused hint(s) waste bandwidth` }] : []),
|
|
542
|
-
...(missingHints.length > 0 ? [{ severity: "info", message: `${missingHints.length} domain(s) could benefit from connection hints` }] : []),
|
|
543
|
-
...(redundantDomains.length > 0 ? [{ severity: "warning", message: `${redundantDomains.length} domain(s) have both preconnect and dns-prefetch (redundant)` }] : []),
|
|
541
|
+
...(totalIssues > 0 ? [{ code: "unused-hints", severity: "warning", message: `${totalIssues} unused hint(s) waste bandwidth` }] : []),
|
|
542
|
+
...(missingHints.length > 0 ? [{ code: "missing-connection-hints", severity: "info", message: `${missingHints.length} domain(s) could benefit from connection hints` }] : []),
|
|
543
|
+
...(redundantDomains.length > 0 ? [{ code: "redundant-hints", severity: "warning", message: `${redundantDomains.length} domain(s) have both preconnect and dns-prefetch (redundant)` }] : []),
|
|
544
544
|
],
|
|
545
545
|
};
|
|
546
546
|
})();
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
if (type.requiresAs && !hint.as) {
|
|
94
94
|
issues.push({
|
|
95
95
|
hint,
|
|
96
|
+
code: "preload-missing-as",
|
|
96
97
|
issue: "Missing 'as' attribute (required for preload)",
|
|
97
98
|
severity: "error",
|
|
98
99
|
fix: `Add as="script|style|font|image|fetch"`,
|
|
@@ -102,6 +103,7 @@
|
|
|
102
103
|
if (hint.as === "font" && !hint.crossorigin) {
|
|
103
104
|
issues.push({
|
|
104
105
|
hint,
|
|
106
|
+
code: "font-preload-no-crossorigin",
|
|
105
107
|
issue: "Font preload without crossorigin (causes double fetch)",
|
|
106
108
|
severity: "error",
|
|
107
109
|
fix: 'Add crossorigin="anonymous"',
|
|
@@ -122,6 +124,7 @@
|
|
|
122
124
|
if (corsResources.length > 0) {
|
|
123
125
|
issues.push({
|
|
124
126
|
hint,
|
|
127
|
+
code: "preconnect-no-crossorigin",
|
|
125
128
|
issue: "Preconnect may need crossorigin for CORS resources",
|
|
126
129
|
severity: "warning",
|
|
127
130
|
fix: 'Consider adding crossorigin="anonymous"',
|
|
@@ -140,6 +143,7 @@
|
|
|
140
143
|
if (!wasUsed) {
|
|
141
144
|
issues.push({
|
|
142
145
|
hint: preload,
|
|
146
|
+
code: "unused-preload",
|
|
143
147
|
issue: "Preloaded resource not used within page load",
|
|
144
148
|
severity: "warning",
|
|
145
149
|
fix: "Remove unused preload or verify it's needed",
|
|
@@ -162,6 +166,7 @@
|
|
|
162
166
|
if (!hasResources && pc.hostname) {
|
|
163
167
|
issues.push({
|
|
164
168
|
hint: pc,
|
|
169
|
+
code: "unused-preconnect",
|
|
165
170
|
issue: `No resources loaded from ${pc.hostname}`,
|
|
166
171
|
severity: "warning",
|
|
167
172
|
fix: "Remove unused preconnect or verify it's needed for future resources",
|
|
@@ -173,6 +178,7 @@
|
|
|
173
178
|
if (preloads.length > 6) {
|
|
174
179
|
issues.push({
|
|
175
180
|
hint: { rel: "preload", href: `${preloads.length} preloads` },
|
|
181
|
+
code: "too-many-preloads",
|
|
176
182
|
issue: `Too many preloads (${preloads.length}) - they compete for bandwidth`,
|
|
177
183
|
severity: "warning",
|
|
178
184
|
fix: "Limit to 5-6 most critical resources",
|
|
@@ -424,6 +430,6 @@
|
|
|
424
430
|
})),
|
|
425
431
|
},
|
|
426
432
|
items: rankedHints.slice(0, MAX_ITEMS).map(h => ({ rel: h.rel, href: h.shortHref, as: h.as, crossorigin: h.crossorigin, fetchpriority: h.fetchpriority })),
|
|
427
|
-
issues: issues.map(i => ({ severity: i.severity, message: `${i.hint.rel}: ${i.issue}. Fix: ${i.fix}
|
|
433
|
+
issues: issues.map(i => ({ code: i.code, severity: i.severity, message: `${i.hint.rel}: ${i.issue}. Fix: ${i.fix}`, ...(i.hint.element ? { target: i.hint.href } : {}) })),
|
|
428
434
|
};
|
|
429
435
|
})();
|
|
@@ -178,8 +178,10 @@
|
|
|
178
178
|
detected.forEach((fw) => {
|
|
179
179
|
if (fw.exceedsThreshold) {
|
|
180
180
|
issuesData.push({
|
|
181
|
+
code: "large-hydration-data",
|
|
181
182
|
severity: "warning",
|
|
182
183
|
message: `${fw.name} hydration data (${Math.round(fw.size / 1024)} KB) exceeds ${Math.round(fw.threshold / 1024)} KB threshold`,
|
|
184
|
+
target: fw.name,
|
|
183
185
|
});
|
|
184
186
|
}
|
|
185
187
|
console.log("");
|
|
@@ -296,13 +298,14 @@
|
|
|
296
298
|
});
|
|
297
299
|
});
|
|
298
300
|
if (largeArrays.length > 0) {
|
|
299
|
-
issuesData.push({ severity: "warning", message: `pageProps has large arrays (${largeArrays.join(", ")}). Consider pagination.` });
|
|
301
|
+
issuesData.push({ code: "large-arrays", severity: "warning", message: `pageProps has large arrays (${largeArrays.join(", ")}). Consider pagination.` });
|
|
300
302
|
}
|
|
301
303
|
if (checkDepth(pageProps)) {
|
|
302
|
-
issuesData.push({ severity: "warning", message: "pageProps contains deeply nested data (more than 5 levels)." });
|
|
304
|
+
issuesData.push({ code: "deep-nesting", severity: "warning", message: "pageProps contains deeply nested data (more than 5 levels)." });
|
|
303
305
|
}
|
|
304
306
|
if (sensitiveKeys.length > 0) {
|
|
305
307
|
issuesData.push({
|
|
308
|
+
code: "sensitive-keys",
|
|
306
309
|
severity: "warning",
|
|
307
310
|
message: `pageProps has keys that look sensitive (${sensitiveKeys.slice(0, 5).join(", ")}). Everything in pageProps is sent to every visitor.`,
|
|
308
311
|
});
|
|
@@ -226,6 +226,7 @@
|
|
|
226
226
|
|
|
227
227
|
if (blockingInHead.length > 0) {
|
|
228
228
|
issues.push({
|
|
229
|
+
code: "blocking-in-head",
|
|
229
230
|
severity: "error",
|
|
230
231
|
message: `${blockingInHead.length} blocking script(s) in <head>`,
|
|
231
232
|
scripts: blockingInHead,
|
|
@@ -235,6 +236,7 @@
|
|
|
235
236
|
|
|
236
237
|
if (thirdPartyBlocking.length > 0) {
|
|
237
238
|
issues.push({
|
|
239
|
+
code: "third-party-blocking",
|
|
238
240
|
severity: "error",
|
|
239
241
|
message: `${thirdPartyBlocking.length} third-party blocking script(s)`,
|
|
240
242
|
scripts: thirdPartyBlocking,
|
|
@@ -245,6 +247,7 @@
|
|
|
245
247
|
const largeBlocking = blocking.filter((s) => s.size > 50 * 1024);
|
|
246
248
|
if (largeBlocking.length > 0) {
|
|
247
249
|
issues.push({
|
|
250
|
+
code: "large-blocking",
|
|
248
251
|
severity: "warning",
|
|
249
252
|
message: `${largeBlocking.length} large blocking script(s) (> 50 KB)`,
|
|
250
253
|
scripts: largeBlocking,
|
|
@@ -262,6 +265,7 @@
|
|
|
262
265
|
|
|
263
266
|
if (couldDefer.length > 0 && blocking.length > couldDefer.length) {
|
|
264
267
|
issues.push({
|
|
268
|
+
code: "could-defer",
|
|
265
269
|
severity: "info",
|
|
266
270
|
message: `${blocking.length - couldDefer.length} blocking script(s) could potentially use defer`,
|
|
267
271
|
fix: "Test with defer attribute to improve parsing performance",
|
|
@@ -391,9 +395,9 @@
|
|
|
391
395
|
durationMs: Math.round(s.duration),
|
|
392
396
|
})),
|
|
393
397
|
issues: [
|
|
394
|
-
...issues.map((i) => ({ severity: i.severity, message: i.message })),
|
|
398
|
+
...issues.map((i) => ({ code: i.code, severity: i.severity, message: i.message })),
|
|
395
399
|
...(scripts.some((s) => !s.sizeKnown)
|
|
396
|
-
? [{ severity: "info", message: `${scripts.filter((s) => !s.sizeKnown).length} script(s) have an unknown size (missing Timing-Allow-Origin); total size is a lower bound` }]
|
|
400
|
+
? [{ code: "unknown-size", severity: "info", message: `${scripts.filter((s) => !s.sizeKnown).length} script(s) have an unknown size (missing Timing-Allow-Origin); total size is a lower bound` }]
|
|
397
401
|
: []),
|
|
398
402
|
],
|
|
399
403
|
};
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
const issues = [];
|
|
84
84
|
if (nav.serverTiming.length === 0) {
|
|
85
85
|
issues.push({
|
|
86
|
+
code: "no-server-timing",
|
|
86
87
|
severity: "info",
|
|
87
88
|
message:
|
|
88
89
|
"The document response sends no Server-Timing header. Add one, for example Server-Timing: db;dur=53, to expose backend timings in DevTools and in the Performance API.",
|
|
@@ -90,12 +91,14 @@
|
|
|
90
91
|
}
|
|
91
92
|
if (corsRestrictedCount > 0) {
|
|
92
93
|
issues.push({
|
|
94
|
+
code: "timing-hidden",
|
|
93
95
|
severity: "info",
|
|
94
96
|
message: `${corsRestrictedCount} cross-origin resource(s) hide their timing because they lack Timing-Allow-Origin, so their Server-Timing cannot be read. Add Timing-Allow-Origin to expose it.`,
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
99
|
if (!earlyHintsReceived) {
|
|
98
100
|
issues.push({
|
|
101
|
+
code: "no-early-hints",
|
|
99
102
|
severity: "info",
|
|
100
103
|
message:
|
|
101
104
|
"No 103 Early Hints response was observed for the document. If the server can send Link preload or preconnect headers before the final response, Early Hints lets the browser start those fetches earlier.",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
count: 0,
|
|
23
23
|
details: {},
|
|
24
24
|
items: [],
|
|
25
|
-
issues: [{ severity: "error", message: "Service Workers not supported in this browser" }],
|
|
25
|
+
issues: [{ code: "unsupported", severity: "error", message: "Service Workers not supported in this browser" }],
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -116,11 +116,12 @@
|
|
|
116
116
|
const issues = [];
|
|
117
117
|
|
|
118
118
|
if (registrations.length === 0) {
|
|
119
|
-
issues.push({ severity: "info", message: "No Service Workers registered" });
|
|
119
|
+
issues.push({ code: "no-service-worker", severity: "info", message: "No Service Workers registered" });
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
if (registrations.length > 0 && !controller) {
|
|
123
123
|
issues.push({
|
|
124
|
+
code: "not-controlled",
|
|
124
125
|
severity: "warning",
|
|
125
126
|
message: "Page is not controlled by a Service Worker (hard reload or first visit). Do a normal reload.",
|
|
126
127
|
});
|
|
@@ -129,25 +130,31 @@
|
|
|
129
130
|
for (const item of items) {
|
|
130
131
|
if (item.hasWaiting) {
|
|
131
132
|
issues.push({
|
|
133
|
+
code: "update-pending",
|
|
132
134
|
severity: "warning",
|
|
133
135
|
message: `Service Worker update pending for ${item.scope}. Call skipWaiting() to activate the new version.`,
|
|
136
|
+
target: item.scope,
|
|
134
137
|
});
|
|
135
138
|
}
|
|
136
139
|
if (item.navigationPreloadEnabled === false) {
|
|
137
140
|
issues.push({
|
|
141
|
+
code: "navigation-preload-disabled",
|
|
138
142
|
severity: "info",
|
|
139
143
|
message: `Navigation Preload is disabled for ${item.scope}. Consider enabling it with registration.navigationPreload.enable().`,
|
|
144
|
+
target: item.scope,
|
|
140
145
|
});
|
|
141
146
|
}
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
if (swOverheadMs !== null && swOverheadMs > 100) {
|
|
145
150
|
issues.push({
|
|
151
|
+
code: "slow-startup",
|
|
146
152
|
severity: "error",
|
|
147
153
|
message: `High SW startup time (${swOverheadMs}ms). Enable Navigation Preload to reduce overhead.`,
|
|
148
154
|
});
|
|
149
155
|
} else if (swOverheadMs !== null && swOverheadMs > 50) {
|
|
150
156
|
issues.push({
|
|
157
|
+
code: "slow-startup",
|
|
151
158
|
severity: "warning",
|
|
152
159
|
message: `Moderate SW startup time (${swOverheadMs}ms)`,
|
|
153
160
|
});
|
|
@@ -155,11 +162,13 @@
|
|
|
155
162
|
|
|
156
163
|
if (cacheHitRate !== null && cacheHitRate < 50) {
|
|
157
164
|
issues.push({
|
|
165
|
+
code: "low-cache-hit-rate",
|
|
158
166
|
severity: "error",
|
|
159
167
|
message: `Low SW cache hit rate (${cacheHitRate}%). Review the caching strategy.`,
|
|
160
168
|
});
|
|
161
169
|
} else if (cacheHitRate !== null && cacheHitRate < 80) {
|
|
162
170
|
issues.push({
|
|
171
|
+
code: "low-cache-hit-rate",
|
|
163
172
|
severity: "warning",
|
|
164
173
|
message: `Moderate SW cache hit rate (${cacheHitRate}%). Review the caching strategy for more resources.`,
|
|
165
174
|
});
|
|
@@ -167,6 +176,7 @@
|
|
|
167
176
|
|
|
168
177
|
if (unknownSource.length > 0) {
|
|
169
178
|
issues.push({
|
|
179
|
+
code: "unknown-source",
|
|
170
180
|
severity: "info",
|
|
171
181
|
message: `${unknownSource.length} intercepted resource(s) are cross-origin without Timing-Allow-Origin, so their source is unknown. The hit rate excludes them.`,
|
|
172
182
|
});
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
parsed = JSON.parse(script.textContent);
|
|
40
40
|
} catch (e) {
|
|
41
41
|
issues.push({
|
|
42
|
+
code: 'invalid-json',
|
|
42
43
|
severity: 'error',
|
|
43
44
|
message: `Speculation rules script #${index + 1} is not valid JSON (${e.message}). The browser ignores the whole script.`,
|
|
44
45
|
});
|
|
@@ -68,12 +69,14 @@
|
|
|
68
69
|
const isPrerender = action === 'prerender' || action === 'prerender_until_script';
|
|
69
70
|
if (isPrerender && source === 'list' && (eagerness === 'immediate' || eagerness === 'eager') && urls.length > EAGER_PRERENDER_LIMIT) {
|
|
70
71
|
issues.push({
|
|
72
|
+
code: 'eager-list-capped',
|
|
71
73
|
severity: 'warning',
|
|
72
74
|
message: `A ${action} rule lists ${urls.length} URLs with eagerness "${eagerness}". Chrome caps eager prerenders, so the extra URLs are not prerendered. Fix: list fewer URLs or use "moderate".`,
|
|
73
75
|
});
|
|
74
76
|
}
|
|
75
77
|
if (isPrerender && source === 'document' && (eagerness === 'immediate' || eagerness === 'eager')) {
|
|
76
78
|
issues.push({
|
|
79
|
+
code: 'eager-document-rule',
|
|
77
80
|
severity: 'warning',
|
|
78
81
|
message: `A document ${action} rule uses eagerness "${eagerness}", which prerenders matching links without a user signal and can waste bandwidth and CPU. Fix: use "moderate" or "conservative".`,
|
|
79
82
|
});
|
|
@@ -83,15 +86,15 @@
|
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
if (scripts.length === 0) {
|
|
86
|
-
issues.push({ severity: 'info', message: 'No speculation rules found in the DOM. Rules delivered in the Speculation-Rules HTTP header are not readable from JavaScript.' });
|
|
89
|
+
issues.push({ code: 'no-rules', severity: 'info', message: 'No speculation rules found in the DOM. Rules delivered in the Speculation-Rules HTTP header are not readable from JavaScript.' });
|
|
87
90
|
} else if (items.length === 0 && !issues.some((i) => i.severity === 'error')) {
|
|
88
|
-
issues.push({ severity: 'info', message: 'Speculation rules scripts were found but contain no prefetch or prerender rules.' });
|
|
91
|
+
issues.push({ code: 'no-prefetch-or-prerender', severity: 'info', message: 'Speculation rules scripts were found but contain no prefetch or prerender rules.' });
|
|
89
92
|
}
|
|
90
93
|
if (!supported && scripts.length > 0) {
|
|
91
|
-
issues.push({ severity: 'info', message: 'This browser does not support speculation rules, so the rules are ignored.' });
|
|
94
|
+
issues.push({ code: 'unsupported', severity: 'info', message: 'This browser does not support speculation rules, so the rules are ignored.' });
|
|
92
95
|
}
|
|
93
96
|
if (activationStart > 0) {
|
|
94
|
-
issues.push({ severity: 'info', message: `This page was prerendered and activated at ${activationStart}ms. Navigation timings are relative to activationStart.` });
|
|
97
|
+
issues.push({ code: 'prerendered', severity: 'info', message: `This page was prerendered and activated at ${activationStart}ms. Navigation timings are relative to activationStart.` });
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
return {
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
corsLimitedAnalysis: corsRestrictedCount > 0,
|
|
169
169
|
details: { avgTtfbMs: null, maxTtfbMs: null, minTtfbMs: null, thirdPartyCount: 0, slowCount: 0, corsRestrictedCount },
|
|
170
170
|
items: [],
|
|
171
|
-
issues: [{ severity: "info", message: "No resources with TTFB data available" }],
|
|
171
|
+
issues: [{ code: "no-resources", severity: "info", message: "No resources with TTFB data available" }],
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
const ttfbVals = resourcesSync.map((r) => r.ttfbMs);
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
const slowestMs = Math.round(slowestSync.duration);
|
|
130
130
|
const share = ttfbValueSync > 0 ? Math.round((slowestSync.duration / ttfbValueSync) * 100) : 0;
|
|
131
131
|
issues.push({
|
|
132
|
+
code: "slow-ttfb",
|
|
132
133
|
severity: rating === "poor" ? "error" : "warning",
|
|
133
134
|
message: `TTFB is ${value} ms (${rating}); ${phaseLabels[slowestSync.key]} takes ${slowestMs} ms of it (${share}%). Fix: ${phaseFixes[slowestSync.key]}.`,
|
|
134
135
|
});
|
|
@@ -187,30 +187,36 @@
|
|
|
187
187
|
const issues = [];
|
|
188
188
|
if (blocking.length > 0) {
|
|
189
189
|
issues.push({
|
|
190
|
+
code: "render-blocking-domains",
|
|
190
191
|
severity: "warning",
|
|
191
192
|
message: `${blocking.length} third-party domain(s) serve render-blocking resources (${listDomains(blocking)}); load them async or defer, or self-host them`,
|
|
192
193
|
});
|
|
193
194
|
}
|
|
194
195
|
heavy.slice(0, MAX_LISTED_DOMAINS).forEach((d) => {
|
|
195
196
|
issues.push({
|
|
197
|
+
code: "heavy-domain",
|
|
196
198
|
severity: "warning",
|
|
197
199
|
message: `${d.domain} scripts take ${d.loafMs} ms of main-thread time in long animation frames (over ${LOAF_WARNING_MS} ms); defer, delay or remove them`,
|
|
200
|
+
target: d.domain,
|
|
198
201
|
});
|
|
199
202
|
});
|
|
200
203
|
if (!renderBlockingSupported && all.length > 0) {
|
|
201
204
|
issues.push({
|
|
205
|
+
code: "render-blocking-unsupported",
|
|
202
206
|
severity: "info",
|
|
203
207
|
message: "renderBlockingStatus is not supported in this browser (Chrome 107+), so which third-party domains block rendering is unknown; run the snippet in Chrome or Edge to check",
|
|
204
208
|
});
|
|
205
209
|
}
|
|
206
210
|
if (loafUnattributed) {
|
|
207
211
|
issues.push({
|
|
212
|
+
code: "loaf-unattributed",
|
|
208
213
|
severity: "info",
|
|
209
214
|
message: `${longFrames} long animation frame(s) took ${longFramesMs} ms, but no third-party script was attributed in them (Long Animation Frames only list scripts over 5 ms), so 0 ms per domain does not rule out third-party cost`,
|
|
210
215
|
});
|
|
211
216
|
}
|
|
212
217
|
if (corsLimitedAnalysis) {
|
|
213
218
|
issues.push({
|
|
219
|
+
code: "unknown-size",
|
|
214
220
|
severity: "info",
|
|
215
221
|
message: `${sizeUnknownCount} third-party request(s) without Timing-Allow-Origin report zero sizes, so the transfer size is a lower bound`,
|
|
216
222
|
});
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
} else {
|
|
84
84
|
// Preload without matching script and not loaded - likely unused
|
|
85
85
|
issues.push({
|
|
86
|
+
code: "orphan-preload",
|
|
86
87
|
type: "orphan",
|
|
87
88
|
severity: "warning",
|
|
88
89
|
preload,
|
|
@@ -137,6 +138,7 @@
|
|
|
137
138
|
});
|
|
138
139
|
} else {
|
|
139
140
|
issues.push({
|
|
141
|
+
code: "async-defer-preload",
|
|
140
142
|
type: "async-defer-preload",
|
|
141
143
|
severity: "error",
|
|
142
144
|
preload,
|
|
@@ -156,6 +158,7 @@
|
|
|
156
158
|
} else if (isModule) {
|
|
157
159
|
// ES modules without async/defer behave like defer by default
|
|
158
160
|
issues.push({
|
|
161
|
+
code: "module-preload",
|
|
159
162
|
type: "module-preload",
|
|
160
163
|
severity: "warning",
|
|
161
164
|
preload,
|
|
@@ -413,8 +416,10 @@
|
|
|
413
416
|
},
|
|
414
417
|
items: Array.from(itemsByUrl.values()).slice(0, 50),
|
|
415
418
|
issues: issues.map(i => ({
|
|
419
|
+
code: i.code,
|
|
416
420
|
severity: i.severity,
|
|
417
421
|
message: i.fix ? `${i.message.replace(/\.$/, "")}. Fix: ${i.fix}` : i.message,
|
|
422
|
+
target: i.url,
|
|
418
423
|
})),
|
|
419
424
|
};
|
|
420
425
|
})();
|