webperf-snippets 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -6
- package/package.json +3 -2
- package/snippets/CoreWebVitals/CLS.js +3 -2
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +8 -9
- package/snippets/CoreWebVitals/LCP-Subparts.js +32 -35
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +14 -13
- package/snippets/Interaction/DOM-Size-and-Depth.js +4 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +2 -0
- package/snippets/Interaction/Input-Latency-Breakdown.js +2 -0
- package/snippets/Interaction/Interactions.js +2 -0
- package/snippets/Interaction/Scroll-Performance.js +2 -0
- package/snippets/Loading/Back-Forward-Cache.js +7 -0
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +4 -2
- package/snippets/Loading/Cache-Strategy-Analysis.js +12 -0
- package/snippets/Loading/Client-Side-Redirect-Detection.js +5 -3
- package/snippets/Loading/Compression-Audit.js +3 -0
- package/snippets/Loading/Content-Visibility.js +2 -2
- package/snippets/Loading/Critical-CSS-Detection.js +6 -0
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +2 -2
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +1 -1
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +1 -1
- package/snippets/Loading/Find-render-blocking-resources.js +1 -1
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +5 -2
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +4 -4
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +3 -3
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +5 -1
- package/snippets/Loading/Inline-Script-Info-and-Size.js +5 -1
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +4 -4
- package/snippets/Loading/Prefetch-Resource-Validation.js +12 -3
- package/snippets/Loading/Priority-Hints-Audit.js +10 -0
- package/snippets/Loading/Resource-Hints-Validation.js +3 -3
- package/snippets/Loading/Resource-Hints.js +7 -1
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +5 -2
- package/snippets/Loading/Script-Loading.js +6 -2
- package/snippets/Loading/Server-Timing-Early-Hints.js +3 -0
- package/snippets/Loading/Service-Worker-Analysis.js +12 -2
- package/snippets/Loading/Speculation-Rules-Inspector.js +7 -4
- package/snippets/Loading/TTFB-Resources.js +1 -1
- package/snippets/Loading/TTFB-Sub-Parts.js +1 -0
- package/snippets/Loading/Third-Party-Impact-by-Domain.js +6 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +5 -0
- package/snippets/Loading/Webfont-Usage-Analyzer.js +13 -14
- package/snippets/Media/Image-Element-Audit.js +16 -13
- package/snippets/Media/Oversized-Images.js +4 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +2 -1
- package/snippets/Media/Video-Element-Audit.js +13 -2
- package/snippets/SCHEMA.md +425 -3
- package/src/baseline.js +225 -0
- package/src/bin.js +68 -7
- package/src/blocking.js +3 -1
- package/src/conditions.js +7 -0
- package/src/mcp/documents.js +41 -21
- package/src/reporters/baseline-view.js +83 -0
- package/src/reporters/human.js +43 -5
- package/src/reporters/markdown.js +46 -6
- package/src/reporters/matrix-view.js +8 -2
|
@@ -119,18 +119,21 @@
|
|
|
119
119
|
const issues = [];
|
|
120
120
|
if (uncompressed.length > 0) {
|
|
121
121
|
issues.push({
|
|
122
|
+
code: "uncompressed",
|
|
122
123
|
severity: "warning",
|
|
123
124
|
message: `${uncompressed.length} text resource(s) served without compression; enabling gzip or brotli could save about ${formatBytes(estimatedSavingsBytes)}`,
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
127
|
if (corsLimitedAnalysis) {
|
|
127
128
|
issues.push({
|
|
129
|
+
code: "unknown-size",
|
|
128
130
|
severity: "info",
|
|
129
131
|
message: `${sizeUnknownCount} cross-origin text resource(s) without Timing-Allow-Origin report zero sizes, so their compression is unknown`,
|
|
130
132
|
});
|
|
131
133
|
}
|
|
132
134
|
if (!contentEncodingSupported && candidates.length > 0) {
|
|
133
135
|
issues.push({
|
|
136
|
+
code: "content-encoding-unavailable",
|
|
134
137
|
severity: "info",
|
|
135
138
|
message: "PerformanceResourceTiming.contentEncoding is not available; compression is inferred from encodedBodySize versus decodedBodySize",
|
|
136
139
|
});
|
|
@@ -348,9 +348,9 @@ window.analyzeContentVisibilityOpportunities = analyzeContentVisibilityOpportuni
|
|
|
348
348
|
issues: [
|
|
349
349
|
...cvResults.autoElements
|
|
350
350
|
.filter(el => el.containIntrinsicSize === "not set" || el.containIntrinsicSize === "none")
|
|
351
|
-
.map(el => ({ severity: "warning", message: `${el.selector}: missing contain-intrinsic-size (CLS risk)
|
|
351
|
+
.map(el => ({ code: "missing-intrinsic-size", severity: "warning", message: `${el.selector}: missing contain-intrinsic-size (CLS risk)`, target: el.selector })),
|
|
352
352
|
...(cvResults.autoElements.length + cvResults.hiddenElements.length === 0
|
|
353
|
-
? [{ severity: "info", message: "No content-visibility usage found. Consider content-visibility: auto on below-the-fold sections to reduce the initial render cost." }]
|
|
353
|
+
? [{ code: "not-used", severity: "info", message: "No content-visibility usage found. Consider content-visibility: auto on below-the-fold sections to reduce the initial render cost." }]
|
|
354
354
|
: []),
|
|
355
355
|
],
|
|
356
356
|
};
|
|
@@ -221,6 +221,7 @@
|
|
|
221
221
|
|
|
222
222
|
if (renderBlockingCSS.length > 0 && inlineStyles.length === 0) {
|
|
223
223
|
issues.push({
|
|
224
|
+
code: "no-inline-critical-css",
|
|
224
225
|
severity: "high",
|
|
225
226
|
message: "No critical CSS inlined: all CSS loads via render-blocking requests",
|
|
226
227
|
suggestion:
|
|
@@ -230,6 +231,7 @@
|
|
|
230
231
|
|
|
231
232
|
if (totalRenderBlockingDecoded > criticalBudget) {
|
|
232
233
|
issues.push({
|
|
234
|
+
code: "render-blocking-over-budget",
|
|
233
235
|
severity: "high",
|
|
234
236
|
message: `Render-blocking CSS (${formatBytes(totalRenderBlockingDecoded)}) exceeds the 14 KB critical budget`,
|
|
235
237
|
suggestion:
|
|
@@ -242,6 +244,7 @@
|
|
|
242
244
|
);
|
|
243
245
|
if (largeUnpreloaded.length > 0) {
|
|
244
246
|
issues.push({
|
|
247
|
+
code: "large-not-preloaded",
|
|
245
248
|
severity: "medium",
|
|
246
249
|
message: `${largeUnpreloaded.length} large render-blocking stylesheet(s) without preload`,
|
|
247
250
|
details: largeUnpreloaded.map(
|
|
@@ -253,6 +256,7 @@
|
|
|
253
256
|
|
|
254
257
|
if (totalInlineBytes > criticalBudget) {
|
|
255
258
|
issues.push({
|
|
259
|
+
code: "inline-over-budget",
|
|
256
260
|
severity: "high",
|
|
257
261
|
message: `Inline CSS (${formatBytes(totalInlineBytes)}) exceeds the 14 KB critical budget`,
|
|
258
262
|
suggestion:
|
|
@@ -263,6 +267,7 @@
|
|
|
263
267
|
const bodyInlineStyles = inlineStyles.filter((s) => !s.inHead);
|
|
264
268
|
if (bodyInlineStyles.length > 0) {
|
|
265
269
|
issues.push({
|
|
270
|
+
code: "styles-in-body",
|
|
266
271
|
severity: "medium",
|
|
267
272
|
message: `${bodyInlineStyles.length} inline style block(s) found in <body>`,
|
|
268
273
|
suggestion:
|
|
@@ -332,6 +337,7 @@
|
|
|
332
337
|
corsBlocked: s.corsBlocked,
|
|
333
338
|
})),
|
|
334
339
|
issues: issues.map(i => ({
|
|
340
|
+
code: i.code,
|
|
335
341
|
severity: i.severity === 'high' ? 'error' : 'warning',
|
|
336
342
|
message: i.message,
|
|
337
343
|
})),
|
|
@@ -265,8 +265,8 @@
|
|
|
265
265
|
isLcpCandidate: image.isLcpCandidate,
|
|
266
266
|
})),
|
|
267
267
|
issues: [
|
|
268
|
-
...(results.summary.lcpAffected ? [{ severity: "error", message: 'LCP candidate image has lazy loading: remove loading="lazy" and add fetchpriority="high"' }] : []),
|
|
269
|
-
...(results.lazyImages.filter(i => !i.isLcpCandidate).length > 0 ? [{ severity: "warning", message: `${results.lazyImages.filter(i => !i.isLcpCandidate).length} above-fold image(s) have lazy loading: remove loading="lazy"` }] : []),
|
|
268
|
+
...(results.summary.lcpAffected ? [{ code: "lcp-lazy", severity: "error", message: 'LCP candidate image has lazy loading: remove loading="lazy" and add fetchpriority="high"' }] : []),
|
|
269
|
+
...(results.lazyImages.filter(i => !i.isLcpCandidate).length > 0 ? [{ code: "lazy-above-fold", severity: "warning", message: `${results.lazyImages.filter(i => !i.isLcpCandidate).length} above-fold image(s) have lazy loading: remove loading="lazy"` }] : []),
|
|
270
270
|
],
|
|
271
271
|
};
|
|
272
272
|
})();
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
};
|
|
150
150
|
}),
|
|
151
151
|
issues: conflictingElements.length > 0
|
|
152
|
-
? [{ severity: "error", message: `${conflictingElements.length} element(s) have conflicting loading="lazy" and fetchpriority="high"` }]
|
|
152
|
+
? [{ code: "lazy-and-high-priority", severity: "error", message: `${conflictingElements.length} element(s) have conflicting loading="lazy" and fetchpriority="high"` }]
|
|
153
153
|
: [],
|
|
154
154
|
};
|
|
155
155
|
})();
|
|
@@ -304,7 +304,7 @@
|
|
|
304
304
|
})),
|
|
305
305
|
],
|
|
306
306
|
issues: totalImages > 0
|
|
307
|
-
? [{ severity: "warning", message: `${totalImages} image(s) outside the viewport are missing loading="lazy"` }]
|
|
307
|
+
? [{ code: "eager-off-viewport", severity: "warning", message: `${totalImages} image(s) outside the viewport are missing loading="lazy"` }]
|
|
308
308
|
: [],
|
|
309
309
|
};
|
|
310
310
|
})();
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
sizeBytes: r.size,
|
|
162
162
|
})),
|
|
163
163
|
issues: blockingResources.length > 0
|
|
164
|
-
? [{ severity: "error", message: `${blockingResources.length} render-blocking resource(s) delay rendering until ${Math.round(lastBlockingEnd)}ms` }]
|
|
164
|
+
? [{ code: "render-blocking", severity: "error", message: `${blockingResources.length} render-blocking resource(s) delay rendering until ${Math.round(lastBlockingEnd)}ms` }]
|
|
165
165
|
: [],
|
|
166
166
|
};
|
|
167
167
|
})();
|
|
@@ -321,14 +321,15 @@
|
|
|
321
321
|
})),
|
|
322
322
|
issues: [
|
|
323
323
|
...(scripts.some((s) => !s.sizeKnown)
|
|
324
|
-
? [{ severity: "info", message: `${scripts.filter((s) => !s.sizeKnown).length} script(s) have an unknown size (missing Timing-Allow-Origin); size totals are a lower bound` }]
|
|
324
|
+
? [{ code: "unknown-size", severity: "info", message: `${scripts.filter((s) => !s.sizeKnown).length} script(s) have an unknown size (missing Timing-Allow-Origin); size totals are a lower bound` }]
|
|
325
325
|
: []),
|
|
326
326
|
...(!renderBlockingSupported && totalScripts > 0
|
|
327
|
-
? [{ severity: "info", message: "renderBlockingStatus is not supported in this browser (Chrome 107+), so which scripts block rendering is unknown; run the snippet in Chrome or Edge to check" }]
|
|
327
|
+
? [{ code: "render-blocking-unsupported", severity: "info", message: "renderBlockingStatus is not supported in this browser (Chrome 107+), so which scripts block rendering is unknown; run the snippet in Chrome or Edge to check" }]
|
|
328
328
|
: []),
|
|
329
329
|
...(thirdMetrics.blocking > 0
|
|
330
330
|
? [
|
|
331
331
|
{
|
|
332
|
+
code: "third-party-blocking",
|
|
332
333
|
severity: "error",
|
|
333
334
|
message: `${thirdMetrics.blocking} render-blocking third-party script(s)`,
|
|
334
335
|
},
|
|
@@ -337,6 +338,7 @@
|
|
|
337
338
|
...(thirdMetrics.hosts.length > 3
|
|
338
339
|
? [
|
|
339
340
|
{
|
|
341
|
+
code: "many-third-party-hosts",
|
|
340
342
|
severity: "warning",
|
|
341
343
|
message: `${thirdMetrics.hosts.length} different third-party hosts require separate DNS lookups`,
|
|
342
344
|
},
|
|
@@ -345,6 +347,7 @@
|
|
|
345
347
|
...(thirdMetrics.totalSize > 100 * 1024
|
|
346
348
|
? [
|
|
347
349
|
{
|
|
350
|
+
code: "large-third-party",
|
|
348
351
|
severity: "warning",
|
|
349
352
|
message: `Third-party scripts total ${Math.round(thirdMetrics.totalSize / 1024)} KB`,
|
|
350
353
|
},
|
|
@@ -328,10 +328,10 @@
|
|
|
328
328
|
},
|
|
329
329
|
items: slowestScripts.slice(0, MAX_ITEMS).map(s => ({ shortName: s.shortName, host: s.host, firstParty: s.firstParty, totalMs: Math.round(s.total), dnsMs: roundOrNull(s.dns), tcpMs: roundOrNull(s.tcp), requestMs: roundOrNull(s.request), responseMs: roundOrNull(s.response), hasTiming: s.hasTiming })),
|
|
330
330
|
issues: [
|
|
331
|
-
...(slowScripts.length > 0 ? [{ severity: "warning", message: `${slowScripts.length} script(s) take over ${slowThreshold}ms to load` }] : []),
|
|
332
|
-
...(hasSlowDns ? [{ severity: "warning", message: "Slow DNS lookups detected (>100ms). Add dns-prefetch or preconnect." }] : []),
|
|
333
|
-
...(hasSlowTcp ? [{ severity: "warning", message: "Slow TCP connections detected (>100ms). Add preconnect for critical origins." }] : []),
|
|
334
|
-
...(hasSlowRequest ? [{ severity: "warning", message: "Slow server response times detected (>200ms). Consider CDN or self-hosting." }] : []),
|
|
331
|
+
...(slowScripts.length > 0 ? [{ code: "slow-scripts", severity: "warning", message: `${slowScripts.length} script(s) take over ${slowThreshold}ms to load` }] : []),
|
|
332
|
+
...(hasSlowDns ? [{ code: "slow-dns", severity: "warning", message: "Slow DNS lookups detected (>100ms). Add dns-prefetch or preconnect." }] : []),
|
|
333
|
+
...(hasSlowTcp ? [{ code: "slow-connection", severity: "warning", message: "Slow TCP connections detected (>100ms). Add preconnect for critical origins." }] : []),
|
|
334
|
+
...(hasSlowRequest ? [{ code: "slow-server", severity: "warning", message: "Slow server response times detected (>200ms). Consider CDN or self-hosting." }] : []),
|
|
335
335
|
],
|
|
336
336
|
};
|
|
337
337
|
})();
|
|
@@ -359,9 +359,9 @@
|
|
|
359
359
|
items: uniqueLoadedFonts.slice(0, 50).map(f => ({ family: f.family, weight: f.weight, style: f.style, display: f.display })),
|
|
360
360
|
usedFonts: usedFonts.slice(0, 50).map(f => ({ family: f.family, weight: f.weight, style: f.style, elements: f.elements })),
|
|
361
361
|
issues: [
|
|
362
|
-
...preloadedNotUsed.map(f => ({ severity: "warning", message: `Preloaded but not used above fold: ${f.name}
|
|
363
|
-
...usedNotPreloaded.map(f => ({ severity: "warning", message: `Used above fold but not preloaded: ${f.family} (${f.weight})` })),
|
|
364
|
-
...preloadedFonts.filter(f => !f.crossorigin).map(f => ({ severity: "error", message: `Font preloaded without crossorigin (double fetch): ${f.name}
|
|
362
|
+
...preloadedNotUsed.map(f => ({ code: "preloaded-unused", severity: "warning", message: `Preloaded but not used above fold: ${f.name}`, target: f.name })),
|
|
363
|
+
...usedNotPreloaded.map(f => ({ code: "used-not-preloaded", severity: "warning", message: `Used above fold but not preloaded: ${f.family} (${f.weight})`, target: `${f.family} (${f.weight})` })),
|
|
364
|
+
...preloadedFonts.filter(f => !f.crossorigin).map(f => ({ code: "preload-no-crossorigin", severity: "error", message: `Font preloaded without crossorigin (double fetch): ${f.name}`, target: f.name })),
|
|
365
365
|
],
|
|
366
366
|
};
|
|
367
367
|
})();
|
|
@@ -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.",
|