webperf-snippets 0.4.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +5 -5
  2. package/package.json +3 -3
  3. package/snippets/CoreWebVitals/CLS.js +1 -1
  4. package/snippets/CoreWebVitals/INP.js +1 -1
  5. package/snippets/CoreWebVitals/LCP-Image-Entropy.js +15 -1
  6. package/snippets/CoreWebVitals/LCP-Subparts.js +47 -18
  7. package/snippets/CoreWebVitals/LCP-Trail.js +35 -10
  8. package/snippets/CoreWebVitals/LCP-Video-Candidate.js +48 -24
  9. package/snippets/CoreWebVitals/LCP.js +37 -15
  10. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +1 -1
  11. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +1 -1
  12. package/snippets/Interaction/DOM-Size-and-Depth.js +1 -1
  13. package/snippets/Interaction/Forced-Synchronous-Layout.js +1 -1
  14. package/snippets/Interaction/Input-Latency-Breakdown.js +1 -1
  15. package/snippets/Interaction/Interactions.js +1 -1
  16. package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +1 -1
  17. package/snippets/Interaction/Long-Animation-Frames-Helpers.js +1 -1
  18. package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +1 -1
  19. package/snippets/Interaction/Long-Animation-Frames.js +1 -1
  20. package/snippets/Interaction/LongTask.js +1 -1
  21. package/snippets/Interaction/Scroll-Performance.js +1 -1
  22. package/snippets/Loading/Back-Forward-Cache.js +19 -2
  23. package/snippets/Loading/CSS-Media-Queries-Analysis.js +121 -456
  24. package/snippets/Loading/Cache-Strategy-Analysis.js +41 -20
  25. package/snippets/Loading/Client-Side-Redirect-Detection.js +11 -3
  26. package/snippets/Loading/Compression-Audit.js +10 -2
  27. package/snippets/Loading/Critical-CSS-Detection.js +11 -3
  28. package/snippets/Loading/Event-Processing-Time.js +1 -1
  29. package/snippets/Loading/FCP.js +12 -4
  30. package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +12 -4
  31. package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +1 -1
  32. package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +10 -2
  33. package/snippets/Loading/Find-render-blocking-resources.js +11 -3
  34. package/snippets/Loading/First-And-Third-Party-Script-Info.js +10 -2
  35. package/snippets/Loading/First-And-Third-Party-Script-Timings.js +10 -2
  36. package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +1 -1
  37. package/snippets/Loading/Inline-CSS-Info-and-Size.js +1 -1
  38. package/snippets/Loading/Inline-Script-Info-and-Size.js +1 -1
  39. package/snippets/Loading/JS-Execution-Time-Breakdown.js +10 -2
  40. package/snippets/Loading/Prefetch-Resource-Validation.js +19 -5
  41. package/snippets/Loading/Priority-Hints-Audit.js +11 -3
  42. package/snippets/Loading/Resource-Hints-Validation.js +10 -2
  43. package/snippets/Loading/Resource-Hints.js +11 -3
  44. package/snippets/Loading/SSR-Hydration-Data-Analysis.js +1 -1
  45. package/snippets/Loading/Script-Loading.js +10 -2
  46. package/snippets/Loading/Server-Timing-Early-Hints.js +10 -2
  47. package/snippets/Loading/Service-Worker-Analysis.js +10 -2
  48. package/snippets/Loading/Speculation-Rules-Inspector.js +1 -1
  49. package/snippets/Loading/TTFB-Resources.js +11 -3
  50. package/snippets/Loading/TTFB-Sub-Parts.js +27 -1
  51. package/snippets/Loading/TTFB.js +1 -1
  52. package/snippets/Loading/Third-Party-Impact-by-Domain.js +10 -2
  53. package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +10 -2
  54. package/snippets/Loading/Webfont-Usage-Analyzer.js +105 -30
  55. package/snippets/Media/Image-Element-Audit.js +19 -2
  56. package/snippets/Media/Oversized-Images.js +28 -3
  57. package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +48 -9
  58. package/snippets/Media/Video-Element-Audit.js +16 -10
  59. package/snippets/README.md +1 -1
  60. package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +1 -1
  61. package/snippets/SCHEMA.md +33 -14
  62. package/snippets/_shared/helpers.js +59 -0
@@ -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.nucliweb.net
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
- // Exclude HEAD/aborted-GET entries injected by this snippet's own fetch calls
186
- // on previous runs. Those entries have no body (encodedBodySize=0, decodedBodySize=0)
187
- // but did transfer bytes (transferSize>0, i.e. only response headers were received).
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.fedex.com = first party for www.fedex.com)
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 — static assets should be cacheable`,
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 — consider long cache or immutable`,
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 — forces revalidation on every request`,
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) — consider a longer cache duration`,
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)}) — enable Brotli or gzip`,
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 — load it once or make it cacheable`,
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 — they can be cached indefinitely since the URL changes on update."
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 — Expires is an outdated mechanism and less reliable."
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 — low performance impact, but worth reviewing for correctness.`
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 — header analysis was limited. Add Timing-Allow-Origin for more insights.`
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.nucliweb.net
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 — likely client-side redirect` }] : []),
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.nucliweb.net
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.nucliweb.net
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 — all CSS loads via render-blocking requests",
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
  });
@@ -1,5 +1,5 @@
1
1
  // Event Processing Time - Analyze page load phases
2
- // https://webperf-snippets.nucliweb.net
2
+ // https://webperf-snippets.net
3
3
 
4
4
  (() => {
5
5
  const phases = [
@@ -1,8 +1,16 @@
1
1
  // FCP Analysis
2
2
  // First Contentful Paint with render-blocking phase breakdown
3
- // https://webperf-snippets.nucliweb.net
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.nucliweb.net/Loading/Find-render-blocking-resources",
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.nucliweb.net
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 — remove loading="lazy" and add fetchpriority="high"' }] : []),
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 — remove loading="lazy"` }] : []),
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,5 +1,5 @@
1
1
  // Find images with contradictory loading="lazy" and fetchpriority="high"
2
- // https://webperf-snippets.nucliweb.net
2
+ // https://webperf-snippets.net
3
3
 
4
4
  (() => {
5
5
  function getSelector(node) {
@@ -1,7 +1,15 @@
1
1
  // Execute after page load without user interaction (scroll, click, etc)
2
- // https://webperf-snippets.nucliweb.net
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.nucliweb.net
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.nucliweb.net
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.nucliweb.net
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,5 +1,5 @@
1
1
  // Font Loading Analysis - Preloaded, Loaded, and Used Above The Fold
2
- // https://webperf-snippets.nucliweb.net
2
+ // https://webperf-snippets.net
3
3
 
4
4
  (() => {
5
5
  // Domains that belong to the site but differ from the page's root domain, such as its own CDN.
@@ -1,5 +1,5 @@
1
1
  // Inline CSS Analysis - Find and analyze all inline style tags
2
- // https://webperf-snippets.nucliweb.net
2
+ // https://webperf-snippets.net
3
3
 
4
4
  (() => {
5
5
  // @shared formatBytes
@@ -1,5 +1,5 @@
1
1
  // Inline Script Analysis - Find and analyze all inline scripts
2
- // https://webperf-snippets.nucliweb.net
2
+ // https://webperf-snippets.net
3
3
 
4
4
  (() => {
5
5
  // @shared formatBytes
@@ -1,7 +1,15 @@
1
1
  // JavaScript Execution Time Breakdown
2
- // https://webperf-snippets.nucliweb.net
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.nucliweb.net
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✅ No prefetch hints found (rel="prefetch").',
12
- "color: #22c55e; font-weight: bold;",
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 { script: "Prefetch-Resource-Validation", status: "ok", count: 0, items: [], issues: [] };
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.nucliweb.net
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.nucliweb.net/Loading/Find-Images-With-Lazy-and-Fetchpriority',
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.nucliweb.net
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.nucliweb.net
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);
@@ -1,5 +1,5 @@
1
1
  // SSR Framework Hydration Data Analysis
2
- // https://webperf-snippets.nucliweb.net
2
+ // https://webperf-snippets.net
3
3
 
4
4
  (() => {
5
5
  // @shared formatBytes