webperf-snippets 0.4.0 → 0.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 +7 -5
- package/package.json +3 -3
- package/snippets/CoreWebVitals/CLS.js +1 -1
- package/snippets/CoreWebVitals/INP.js +1 -1
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +15 -1
- package/snippets/CoreWebVitals/LCP-Subparts.js +47 -18
- package/snippets/CoreWebVitals/LCP-Trail.js +35 -10
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +48 -24
- package/snippets/CoreWebVitals/LCP.js +37 -15
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +1 -1
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +1 -1
- package/snippets/Interaction/DOM-Size-and-Depth.js +1 -1
- package/snippets/Interaction/Forced-Synchronous-Layout.js +1 -1
- package/snippets/Interaction/Input-Latency-Breakdown.js +1 -1
- package/snippets/Interaction/Interactions.js +1 -1
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +1 -1
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +1 -1
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +1 -1
- package/snippets/Interaction/Long-Animation-Frames.js +1 -1
- package/snippets/Interaction/LongTask.js +1 -1
- package/snippets/Interaction/Scroll-Performance.js +1 -1
- package/snippets/Loading/Back-Forward-Cache.js +19 -2
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +121 -456
- package/snippets/Loading/Cache-Strategy-Analysis.js +41 -20
- package/snippets/Loading/Client-Side-Redirect-Detection.js +11 -3
- package/snippets/Loading/Compression-Audit.js +10 -2
- package/snippets/Loading/Critical-CSS-Detection.js +11 -3
- package/snippets/Loading/Event-Processing-Time.js +1 -1
- package/snippets/Loading/FCP.js +12 -4
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +12 -4
- 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 +10 -2
- package/snippets/Loading/Find-render-blocking-resources.js +11 -3
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +10 -2
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +10 -2
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +1 -1
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +1 -1
- package/snippets/Loading/Inline-Script-Info-and-Size.js +1 -1
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +10 -2
- package/snippets/Loading/Prefetch-Resource-Validation.js +19 -5
- package/snippets/Loading/Priority-Hints-Audit.js +11 -3
- package/snippets/Loading/Resource-Hints-Validation.js +10 -2
- package/snippets/Loading/Resource-Hints.js +11 -3
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +1 -1
- package/snippets/Loading/Script-Loading.js +10 -2
- package/snippets/Loading/Server-Timing-Early-Hints.js +10 -2
- package/snippets/Loading/Service-Worker-Analysis.js +10 -2
- package/snippets/Loading/Speculation-Rules-Inspector.js +1 -1
- package/snippets/Loading/TTFB-Resources.js +11 -3
- package/snippets/Loading/TTFB-Sub-Parts.js +27 -1
- package/snippets/Loading/TTFB.js +1 -1
- package/snippets/Loading/Third-Party-Impact-by-Domain.js +10 -2
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +10 -2
- package/snippets/Loading/Webfont-Usage-Analyzer.js +105 -30
- package/snippets/Media/Image-Element-Audit.js +19 -2
- package/snippets/Media/Oversized-Images.js +28 -3
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +48 -9
- package/snippets/Media/Video-Element-Audit.js +16 -10
- package/snippets/README.md +1 -1
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +1 -1
- package/snippets/SCHEMA.md +33 -14
- package/snippets/_shared/helpers.js +59 -0
- package/src/interactions.js +27 -0
- package/src/runner.js +5 -3
- package/src/tracking.js +4 -4
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
// Cache Strategy Analysis
|
|
2
2
|
// Analyzes HTTP caching strategies, CDN hit rates, and cache anti-patterns across all page resources
|
|
3
|
-
// https://webperf-snippets.
|
|
3
|
+
// https://webperf-snippets.net
|
|
4
4
|
|
|
5
5
|
(async () => {
|
|
6
|
+
// @shared ownRequestUrl
|
|
7
|
+
// The URL a snippet requests when it reads a file of the page itself (fetch, a HEAD request,
|
|
8
|
+
// new Image()). The fragment never reaches the server or the HTTP cache key, so the file still
|
|
9
|
+
// comes from the cache, but it stays in the name of the Resource Timing entry of the request,
|
|
10
|
+
// which is how isOwnRequest leaves these requests out of what the page loaded.
|
|
11
|
+
function ownRequestUrl(url) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(url, location.href);
|
|
14
|
+
if (!/^https?:$/.test(parsed.protocol)) return url;
|
|
15
|
+
parsed.hash = "webperf-snippets";
|
|
16
|
+
return parsed.href;
|
|
17
|
+
} catch {
|
|
18
|
+
return url;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
// @end-shared ownRequestUrl
|
|
22
|
+
|
|
23
|
+
// @shared isOwnRequest
|
|
24
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
25
|
+
// did not make it, so no snippet counts it.
|
|
26
|
+
function isOwnRequest(entry) {
|
|
27
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
28
|
+
}
|
|
29
|
+
// @end-shared isOwnRequest
|
|
30
|
+
|
|
6
31
|
// Domains that belong to the site but differ from the page's root domain, such as its own CDN.
|
|
7
32
|
// They count as first party. Example: const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];
|
|
8
33
|
const OWN_DOMAINS = [];
|
|
@@ -182,13 +207,9 @@
|
|
|
182
207
|
|
|
183
208
|
// --- Phase 1: Gather resource entries ---
|
|
184
209
|
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
// Real page resources always have encodedBodySize > 0 (or transferSize = 0 if cached).
|
|
189
|
-
const resources = performance.getEntriesByType("resource").filter(
|
|
190
|
-
(r) => !(r.encodedBodySize === 0 && r.decodedBodySize === 0 && r.transferSize > 0)
|
|
191
|
-
);
|
|
210
|
+
// The HEAD requests this snippet makes in Phase 2 carry the ownRequestUrl tag, so a second run
|
|
211
|
+
// leaves them out
|
|
212
|
+
const resources = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
192
213
|
|
|
193
214
|
// Detect duplicate URLs
|
|
194
215
|
const urlCounts = new Map();
|
|
@@ -236,7 +257,7 @@
|
|
|
236
257
|
// --- Phase 2: Fetch headers for same-origin static assets ---
|
|
237
258
|
|
|
238
259
|
// Scope: true same-origin only (same scheme+host+port, not just root domain).
|
|
239
|
-
// isFirstParty() matches root domains (api.
|
|
260
|
+
// isFirstParty() matches root domains (api.example.com = first party for www.example.com)
|
|
240
261
|
// but the browser enforces CORS per full origin, causing console errors.
|
|
241
262
|
// Also limit to static asset types — API endpoints and iframes tend to reject HEAD
|
|
242
263
|
// (503/403) and pollute the console without adding useful cache strategy data.
|
|
@@ -255,7 +276,7 @@
|
|
|
255
276
|
await Promise.allSettled(
|
|
256
277
|
batch.map(async (url) => {
|
|
257
278
|
try {
|
|
258
|
-
const resp = await fetch(url, { method: "HEAD", cache: "no-store" });
|
|
279
|
+
const resp = await fetch(ownRequestUrl(url), { method: "HEAD", cache: "no-store" });
|
|
259
280
|
if (!resp.ok) {
|
|
260
281
|
headerCache.set(url, null);
|
|
261
282
|
return;
|
|
@@ -317,7 +338,7 @@
|
|
|
317
338
|
entry.antiPatterns.push({
|
|
318
339
|
id: "static-no-store",
|
|
319
340
|
severity: "error",
|
|
320
|
-
message: `${type} asset with no-store
|
|
341
|
+
message: `${type} asset with no-store; static assets should be cacheable`,
|
|
321
342
|
});
|
|
322
343
|
}
|
|
323
344
|
|
|
@@ -325,7 +346,7 @@
|
|
|
325
346
|
entry.antiPatterns.push({
|
|
326
347
|
id: "versioned-short-cache",
|
|
327
348
|
severity: "warning",
|
|
328
|
-
message: `Versioned URL (hash detected) but max-age=${maxAge}s
|
|
349
|
+
message: `Versioned URL (hash detected) but max-age=${maxAge}s: consider long cache or immutable`,
|
|
329
350
|
});
|
|
330
351
|
}
|
|
331
352
|
|
|
@@ -360,7 +381,7 @@
|
|
|
360
381
|
entry.antiPatterns.push({
|
|
361
382
|
id: "maxage-zero-static",
|
|
362
383
|
severity: "warning",
|
|
363
|
-
message: `Static asset with max-age=0
|
|
384
|
+
message: `Static asset with max-age=0, which forces revalidation on every request`,
|
|
364
385
|
});
|
|
365
386
|
}
|
|
366
387
|
|
|
@@ -374,7 +395,7 @@
|
|
|
374
395
|
entry.antiPatterns.push({
|
|
375
396
|
id: "short-cache-static",
|
|
376
397
|
severity: "warning",
|
|
377
|
-
message: `Static asset with max-age=${maxAge}s (< 1h)
|
|
398
|
+
message: `Static asset with max-age=${maxAge}s (< 1h): consider a longer cache duration`,
|
|
378
399
|
});
|
|
379
400
|
}
|
|
380
401
|
}
|
|
@@ -452,7 +473,7 @@
|
|
|
452
473
|
entry.antiPatterns.push({
|
|
453
474
|
id: "uncompressed",
|
|
454
475
|
severity: "warning",
|
|
455
|
-
message: `${entry.type} sent uncompressed (${formatBytes(entry.encodedBodySize)} on the wire for ${formatBytes(entry.decodedBodySize)})
|
|
476
|
+
message: `${entry.type} sent uncompressed (${formatBytes(entry.encodedBodySize)} on the wire for ${formatBytes(entry.decodedBodySize)}): enable Brotli or gzip`,
|
|
456
477
|
});
|
|
457
478
|
}
|
|
458
479
|
for (const duplicate of costlyDuplicates) {
|
|
@@ -460,7 +481,7 @@
|
|
|
460
481
|
entries.find((e) => e.url === duplicate.url).antiPatterns.push({
|
|
461
482
|
id: "duplicate-uncached",
|
|
462
483
|
severity: "warning",
|
|
463
|
-
message: `Loaded ${duplicate.count} times, ${duplicate.uncachedLoads} from the network
|
|
484
|
+
message: `Loaded ${duplicate.count} times, ${duplicate.uncachedLoads} from the network: load it once or make it cacheable`,
|
|
464
485
|
});
|
|
465
486
|
}
|
|
466
487
|
|
|
@@ -778,12 +799,12 @@
|
|
|
778
799
|
}
|
|
779
800
|
if (shortCacheEntries.filter((e) => e.hasHash).length > 0) {
|
|
780
801
|
recommendations.push(
|
|
781
|
-
"🟡 Versioned files (with content hash) should use long cache or Cache-Control: immutable
|
|
802
|
+
"🟡 Versioned files (with content hash) should use long cache or Cache-Control: immutable; they can be cached indefinitely since the URL changes on update."
|
|
782
803
|
);
|
|
783
804
|
}
|
|
784
805
|
if (allAntiPatterns.some((p) => p.id === "expires-without-cc")) {
|
|
785
806
|
recommendations.push(
|
|
786
|
-
"🟡 Replace Expires headers with Cache-Control: max-age
|
|
807
|
+
"🟡 Replace Expires headers with Cache-Control: max-age. Expires is an outdated mechanism and less reliable."
|
|
787
808
|
);
|
|
788
809
|
}
|
|
789
810
|
if (cacheEfficiencyPercent !== null && cacheEfficiencyPercent < 50) {
|
|
@@ -812,12 +833,12 @@
|
|
|
812
833
|
);
|
|
813
834
|
} else if (cachedDuplicates.length > 0) {
|
|
814
835
|
recommendations.push(
|
|
815
|
-
`ℹ️ ${cachedDuplicates.length} resources are requested multiple times but served from cache
|
|
836
|
+
`ℹ️ ${cachedDuplicates.length} resources are requested multiple times but served from cache; low performance impact, but worth reviewing for correctness.`
|
|
816
837
|
);
|
|
817
838
|
}
|
|
818
839
|
if (corsRestricted > 0 && headersAnalyzed === 0) {
|
|
819
840
|
recommendations.push(
|
|
820
|
-
`ℹ️ All resources are cross-origin CORS-restricted
|
|
841
|
+
`ℹ️ All resources are cross-origin CORS-restricted, so header analysis was limited. Add Timing-Allow-Origin for more insights.`
|
|
821
842
|
);
|
|
822
843
|
}
|
|
823
844
|
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// Client-Side Redirect Detection
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(async () => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
const navEntries = performance.getEntriesByType('navigation');
|
|
6
14
|
|
|
7
15
|
if (navEntries.length === 0) {
|
|
@@ -14,7 +22,7 @@
|
|
|
14
22
|
|
|
15
23
|
const navEntry = navEntries[0];
|
|
16
24
|
const currentURL = new URL(window.location.href);
|
|
17
|
-
const resources = performance.getEntriesByType('resource');
|
|
25
|
+
const resources = performance.getEntriesByType('resource').filter((e) => !isOwnRequest(e));
|
|
18
26
|
|
|
19
27
|
// Check for document navigations (only navigation type, exclude iframes)
|
|
20
28
|
// Filter out third-party iframes (analytics, ads, etc.)
|
|
@@ -330,7 +338,7 @@
|
|
|
330
338
|
},
|
|
331
339
|
issues: [
|
|
332
340
|
...(serverRedirects > 0 ? [{ severity: "warning", message: `${serverRedirects} server-side redirect(s) detected (${Math.round(redirectTime)}ms)` }] : []),
|
|
333
|
-
...(documentNavigations.length > 0 ? [{ severity: "error", message: `${documentNavigations.length} same-origin document navigation(s) detected
|
|
341
|
+
...(documentNavigations.length > 0 ? [{ severity: "error", message: `${documentNavigations.length} same-origin document navigation(s) detected, likely a client-side redirect` }] : []),
|
|
334
342
|
...indicators.filter(i => i.severity === "warning").map(i => ({ severity: "warning", message: i.type })),
|
|
335
343
|
],
|
|
336
344
|
};
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// Compression Audit
|
|
2
2
|
// Finds text-based resources served without HTTP compression and estimates the savings
|
|
3
|
-
// https://webperf-snippets.
|
|
3
|
+
// https://webperf-snippets.net
|
|
4
4
|
|
|
5
5
|
(() => {
|
|
6
|
+
// @shared isOwnRequest
|
|
7
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
8
|
+
// did not make it, so no snippet counts it.
|
|
9
|
+
function isOwnRequest(entry) {
|
|
10
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
11
|
+
}
|
|
12
|
+
// @end-shared isOwnRequest
|
|
13
|
+
|
|
6
14
|
const MIN_BYTES = 1024; // Below this, compression saves too little to matter
|
|
7
15
|
const MAX_ITEMS = 50;
|
|
8
16
|
|
|
@@ -46,7 +54,7 @@
|
|
|
46
54
|
return null;
|
|
47
55
|
};
|
|
48
56
|
|
|
49
|
-
const entries = [...performance.getEntriesByType("navigation"), ...performance.getEntriesByType("resource")];
|
|
57
|
+
const entries = [...performance.getEntriesByType("navigation"), ...performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e))];
|
|
50
58
|
const contentEncodingSupported = entries.some((e) => "contentEncoding" in e);
|
|
51
59
|
|
|
52
60
|
const candidates = [];
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// Critical CSS Detection
|
|
2
2
|
// Analyzes render-blocking stylesheets, inline CSS budget, and loading strategy
|
|
3
|
-
// https://webperf-snippets.
|
|
3
|
+
// https://webperf-snippets.net
|
|
4
4
|
|
|
5
5
|
(() => {
|
|
6
|
+
// @shared isOwnRequest
|
|
7
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
8
|
+
// did not make it, so no snippet counts it.
|
|
9
|
+
function isOwnRequest(entry) {
|
|
10
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
11
|
+
}
|
|
12
|
+
// @end-shared isOwnRequest
|
|
13
|
+
|
|
6
14
|
// @shared formatBytes
|
|
7
15
|
function formatBytes(bytes) {
|
|
8
16
|
if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
|
|
@@ -20,7 +28,7 @@
|
|
|
20
28
|
document.querySelectorAll('link[rel="stylesheet"]')
|
|
21
29
|
);
|
|
22
30
|
|
|
23
|
-
const perfEntries = performance.getEntriesByType("resource");
|
|
31
|
+
const perfEntries = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
24
32
|
|
|
25
33
|
const externalCSSData = linkStylesheets.map((link) => {
|
|
26
34
|
const href = link.href;
|
|
@@ -214,7 +222,7 @@
|
|
|
214
222
|
if (renderBlockingCSS.length > 0 && inlineStyles.length === 0) {
|
|
215
223
|
issues.push({
|
|
216
224
|
severity: "high",
|
|
217
|
-
message: "No critical CSS inlined
|
|
225
|
+
message: "No critical CSS inlined: all CSS loads via render-blocking requests",
|
|
218
226
|
suggestion:
|
|
219
227
|
'Extract above-the-fold styles into a <style> tag in <head> and defer the rest',
|
|
220
228
|
});
|
package/snippets/Loading/FCP.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
// FCP Analysis
|
|
2
2
|
// First Contentful Paint with render-blocking phase breakdown
|
|
3
|
-
// https://webperf-snippets.
|
|
3
|
+
// https://webperf-snippets.net
|
|
4
4
|
|
|
5
5
|
(() => {
|
|
6
|
+
// @shared isOwnRequest
|
|
7
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
8
|
+
// did not make it, so no snippet counts it.
|
|
9
|
+
function isOwnRequest(entry) {
|
|
10
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
11
|
+
}
|
|
12
|
+
// @end-shared isOwnRequest
|
|
13
|
+
|
|
6
14
|
const valueToRating = (ms) =>
|
|
7
15
|
ms <= 1800 ? "good" : ms <= 3000 ? "needs-improvement" : "poor";
|
|
8
16
|
|
|
@@ -28,7 +36,7 @@
|
|
|
28
36
|
const ttfb = Math.max(0, (navEntry?.responseStart ?? 0) - activationStart);
|
|
29
37
|
|
|
30
38
|
// Render-blocking resources (Chrome 107+, requires renderBlockingStatus API)
|
|
31
|
-
const resources = performance.getEntriesByType("resource");
|
|
39
|
+
const resources = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
32
40
|
const blockingResources = resources.filter(
|
|
33
41
|
(r) => r.renderBlockingStatus === "blocking"
|
|
34
42
|
);
|
|
@@ -72,7 +80,7 @@
|
|
|
72
80
|
});
|
|
73
81
|
console.log("");
|
|
74
82
|
console.log(
|
|
75
|
-
"%c→ Find Render-Blocking Resources: %chttps://webperf-snippets.
|
|
83
|
+
"%c→ Find Render-Blocking Resources: %chttps://webperf-snippets.net/Loading/Find-render-blocking-resources",
|
|
76
84
|
"color: #3b82f6;",
|
|
77
85
|
"color: #3b82f6; text-decoration: underline;"
|
|
78
86
|
);
|
|
@@ -98,7 +106,7 @@
|
|
|
98
106
|
// render-blocking resource, then the rest until the paint
|
|
99
107
|
const ttfbSync = Math.max(0, (navEntrySync?.responseStart ?? 0) - activationStartSync);
|
|
100
108
|
const blockingSync = performance
|
|
101
|
-
.getEntriesByType("resource")
|
|
109
|
+
.getEntriesByType("resource").filter((e) => !isOwnRequest(e))
|
|
102
110
|
.filter((r) => r.renderBlockingStatus === "blocking");
|
|
103
111
|
const lastBlockingEndSync = Math.max(
|
|
104
112
|
ttfbSync,
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// Detect lazy loaded images above the fold
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(function() {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
function getSelector(node) {
|
|
6
14
|
let sel = "";
|
|
7
15
|
try {
|
|
@@ -49,7 +57,7 @@
|
|
|
49
57
|
function getFileSize(src) {
|
|
50
58
|
if (!src) return null;
|
|
51
59
|
try {
|
|
52
|
-
const entries = performance.getEntriesByType("resource");
|
|
60
|
+
const entries = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
53
61
|
const entry = entries.find(e => e.name === src || e.name.includes(src.split("/").pop()));
|
|
54
62
|
if (entry && entry.transferSize) {
|
|
55
63
|
return (entry.transferSize / 1024).toFixed(1) + " KB";
|
|
@@ -257,8 +265,8 @@
|
|
|
257
265
|
isLcpCandidate: image.isLcpCandidate,
|
|
258
266
|
})),
|
|
259
267
|
issues: [
|
|
260
|
-
...(results.summary.lcpAffected ? [{ severity: "error", message: 'LCP candidate image has lazy loading
|
|
261
|
-
...(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
|
|
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"` }] : []),
|
|
262
270
|
],
|
|
263
271
|
};
|
|
264
272
|
})();
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// Execute after page load without user interaction (scroll, click, etc)
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(function () {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
function getSelector(node) {
|
|
6
14
|
let sel = "";
|
|
7
15
|
try {
|
|
@@ -69,7 +77,7 @@
|
|
|
69
77
|
const src = imgElement.currentSrc || imgElement.src;
|
|
70
78
|
if (!src || src.startsWith("data:")) return 0;
|
|
71
79
|
|
|
72
|
-
const perfEntries = performance.getEntriesByType("resource");
|
|
80
|
+
const perfEntries = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
73
81
|
const imgEntry = perfEntries.find((entry) => entry.name === src);
|
|
74
82
|
|
|
75
83
|
if (imgEntry) {
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
// Find render-blocking resources
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
// Check browser support
|
|
6
|
-
const testEntry = performance.getEntriesByType("resource")[0];
|
|
14
|
+
const testEntry = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e))[0];
|
|
7
15
|
if (testEntry && !("renderBlockingStatus" in testEntry)) {
|
|
8
16
|
console.log(
|
|
9
17
|
"%c⚠️ renderBlockingStatus not supported in this browser",
|
|
@@ -15,7 +23,7 @@
|
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
const blockingResources = performance
|
|
18
|
-
.getEntriesByType("resource")
|
|
26
|
+
.getEntriesByType("resource").filter((e) => !isOwnRequest(e))
|
|
19
27
|
.filter((entry) => entry.renderBlockingStatus === "blocking")
|
|
20
28
|
.map((entry) => {
|
|
21
29
|
const url = new URL(entry.name);
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// First and Third Party Script Analysis
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
// Domains that belong to the site but differ from the page's root domain, such as its own CDN.
|
|
6
14
|
// They count as first party. Example: const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];
|
|
7
15
|
const OWN_DOMAINS = [];
|
|
@@ -54,7 +62,7 @@
|
|
|
54
62
|
|
|
55
63
|
// Gather script data
|
|
56
64
|
const scripts = performance
|
|
57
|
-
.getEntriesByType("resource")
|
|
65
|
+
.getEntriesByType("resource").filter((e) => !isOwnRequest(e))
|
|
58
66
|
.filter((r) => r.initiatorType === "script")
|
|
59
67
|
.map((r) => {
|
|
60
68
|
const url = new URL(r.name);
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// First and Third Party Script Timings Analysis
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
// Domains that belong to the site but differ from the page's root domain, such as its own CDN.
|
|
6
14
|
// They count as first party. Example: const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];
|
|
7
15
|
const OWN_DOMAINS = [];
|
|
@@ -44,7 +52,7 @@
|
|
|
44
52
|
|
|
45
53
|
// Gather script timing data
|
|
46
54
|
const scripts = performance
|
|
47
|
-
.getEntriesByType("resource")
|
|
55
|
+
.getEntriesByType("resource").filter((e) => !isOwnRequest(e))
|
|
48
56
|
.filter((r) => r.initiatorType === "script")
|
|
49
57
|
.map((r) => {
|
|
50
58
|
const url = new URL(r.name);
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// JavaScript Execution Time Breakdown
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
const formatMs = (ms) => (ms > 0 ? ms.toFixed(0) + "ms" : "-");
|
|
6
14
|
// @shared formatBytes
|
|
7
15
|
function formatBytes(bytes) {
|
|
@@ -25,7 +33,7 @@
|
|
|
25
33
|
const loadEvent = nav?.loadEventEnd || 0;
|
|
26
34
|
// 2. Script resource timing (download phase)
|
|
27
35
|
const scriptResources = performance
|
|
28
|
-
.getEntriesByType("resource")
|
|
36
|
+
.getEntriesByType("resource").filter((e) => !isOwnRequest(e))
|
|
29
37
|
.filter((r) => r.initiatorType === "script")
|
|
30
38
|
.map((r) => {
|
|
31
39
|
let shortName;
|
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
// Prefetch Resource Validation
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
// Get all prefetch hints
|
|
6
14
|
const prefetchLinks = Array.from(document.querySelectorAll('link[rel="prefetch"]'));
|
|
7
15
|
|
|
8
16
|
if (prefetchLinks.length === 0) {
|
|
9
17
|
// Early return is safe here - console.group hasn't been opened yet
|
|
10
18
|
console.log(
|
|
11
|
-
'%c
|
|
12
|
-
"color: #
|
|
19
|
+
'%cℹ️ No prefetch hints on this page (rel="prefetch"), nothing to validate.',
|
|
20
|
+
"color: #3b82f6; font-weight: bold;",
|
|
13
21
|
);
|
|
14
22
|
console.log(
|
|
15
23
|
"%cℹ️ Prefetch is for future navigation resources. Use it sparingly for predictable user journeys.",
|
|
16
24
|
"color: #3b82f6;",
|
|
17
25
|
);
|
|
18
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
script: "Prefetch-Resource-Validation",
|
|
28
|
+
status: "ok",
|
|
29
|
+
count: 0,
|
|
30
|
+
items: [],
|
|
31
|
+
issues: [{ severity: "info", message: 'No prefetch hints on this page (rel="prefetch"), so there was nothing to validate.' }],
|
|
32
|
+
};
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
// Get performance entries for all resources
|
|
22
|
-
const performanceEntries = performance.getEntriesByType("resource");
|
|
36
|
+
const performanceEntries = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
23
37
|
|
|
24
38
|
// Pre-normalize URLs for efficient matching (avoids repeated URL parsing in nested loops)
|
|
25
39
|
const entryUrlMap = new Map();
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// Priority Hints Audit
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
function isInViewport(el) {
|
|
6
14
|
const rect = el.getBoundingClientRect();
|
|
7
15
|
return (
|
|
@@ -36,7 +44,7 @@
|
|
|
36
44
|
|
|
37
45
|
// Build performance entry map for timing lookups
|
|
38
46
|
const entryMap = new Map();
|
|
39
|
-
performance.getEntriesByType("resource").forEach((e) => {
|
|
47
|
+
performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e)).forEach((e) => {
|
|
40
48
|
try {
|
|
41
49
|
entryMap.set(new URL(e.name, location.origin).href, e);
|
|
42
50
|
} catch {}
|
|
@@ -309,7 +317,7 @@
|
|
|
309
317
|
|
|
310
318
|
console.log("");
|
|
311
319
|
console.log(
|
|
312
|
-
'%c💡 Related: "Find Images With Lazy and Fetchpriority" detects the lazy+high conflict on images. https://webperf-snippets.
|
|
320
|
+
'%c💡 Related: "Find Images With Lazy and Fetchpriority" detects the lazy+high conflict on images. https://webperf-snippets.net/Loading/Find-Images-With-Lazy-and-Fetchpriority',
|
|
313
321
|
"color: #6b7280;"
|
|
314
322
|
);
|
|
315
323
|
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// Resource Hints Validation
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
console.group("%c🔍 Resource Hints Validation", "font-weight: bold; font-size: 14px;");
|
|
6
14
|
|
|
7
15
|
// Check if page is fully loaded
|
|
@@ -18,7 +26,7 @@
|
|
|
18
26
|
const dnsPrefetchLinks = Array.from(document.querySelectorAll('link[rel="dns-prefetch"]'));
|
|
19
27
|
|
|
20
28
|
// Get all loaded resources from Performance API
|
|
21
|
-
const resources = performance.getEntriesByType("resource");
|
|
29
|
+
const resources = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
22
30
|
|
|
23
31
|
// Helper: Extract domain from URL
|
|
24
32
|
const getDomain = (url) => {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
// Resource Hints Analysis
|
|
2
|
-
// https://webperf-snippets.
|
|
2
|
+
// https://webperf-snippets.net
|
|
3
3
|
|
|
4
4
|
(() => {
|
|
5
|
+
// @shared isOwnRequest
|
|
6
|
+
// True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
|
|
7
|
+
// did not make it, so no snippet counts it.
|
|
8
|
+
function isOwnRequest(entry) {
|
|
9
|
+
return entry.name.endsWith("#webperf-snippets");
|
|
10
|
+
}
|
|
11
|
+
// @end-shared isOwnRequest
|
|
12
|
+
|
|
5
13
|
// @shared formatBytes
|
|
6
14
|
function formatBytes(bytes) {
|
|
7
15
|
if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
|
|
@@ -103,7 +111,7 @@
|
|
|
103
111
|
if (type.rel === "preconnect" && !hint.crossorigin) {
|
|
104
112
|
// Check if we load CORS resources from this origin
|
|
105
113
|
const corsResources = performance
|
|
106
|
-
.getEntriesByType("resource")
|
|
114
|
+
.getEntriesByType("resource").filter((e) => !isOwnRequest(e))
|
|
107
115
|
.filter(
|
|
108
116
|
(r) =>
|
|
109
117
|
r.name.includes(hostname) &&
|
|
@@ -125,7 +133,7 @@
|
|
|
125
133
|
|
|
126
134
|
// Check for unused preloads
|
|
127
135
|
const preloads = allHints.filter((h) => h.rel === "preload");
|
|
128
|
-
const loadedResources = performance.getEntriesByType("resource");
|
|
136
|
+
const loadedResources = performance.getEntriesByType("resource").filter((e) => !isOwnRequest(e));
|
|
129
137
|
|
|
130
138
|
preloads.forEach((preload) => {
|
|
131
139
|
const wasUsed = loadedResources.some((r) => r.name === preload.href);
|