webperf-snippets 0.1.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 (67) hide show
  1. package/README.md +139 -0
  2. package/package.json +54 -0
  3. package/snippets/CoreWebVitals/CLS.js +88 -0
  4. package/snippets/CoreWebVitals/INP.js +432 -0
  5. package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
  6. package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
  7. package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
  8. package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
  9. package/snippets/CoreWebVitals/LCP.js +135 -0
  10. package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
  11. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
  12. package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
  13. package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
  14. package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
  15. package/snippets/Interaction/Interactions.js +220 -0
  16. package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
  17. package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
  18. package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
  19. package/snippets/Interaction/Long-Animation-Frames.js +298 -0
  20. package/snippets/Interaction/LongTask.js +203 -0
  21. package/snippets/Interaction/Scroll-Performance.js +359 -0
  22. package/snippets/Interaction/WORKFLOWS.md +205 -0
  23. package/snippets/Loading/Back-Forward-Cache.js +473 -0
  24. package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
  25. package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
  26. package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
  27. package/snippets/Loading/Content-Visibility.js +352 -0
  28. package/snippets/Loading/Critical-CSS-Detection.js +329 -0
  29. package/snippets/Loading/Event-Processing-Time.js +179 -0
  30. package/snippets/Loading/FCP.js +101 -0
  31. package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
  32. package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
  33. package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
  34. package/snippets/Loading/Find-render-blocking-resources.js +161 -0
  35. package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
  36. package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
  37. package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
  38. package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
  39. package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
  40. package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
  41. package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
  42. package/snippets/Loading/Priority-Hints-Audit.js +336 -0
  43. package/snippets/Loading/Resource-Hints-Validation.js +503 -0
  44. package/snippets/Loading/Resource-Hints.js +400 -0
  45. package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
  46. package/snippets/Loading/Script-Loading.js +332 -0
  47. package/snippets/Loading/Service-Worker-Analysis.js +192 -0
  48. package/snippets/Loading/TTFB-Resources.js +115 -0
  49. package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
  50. package/snippets/Loading/TTFB.js +37 -0
  51. package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
  52. package/snippets/Loading/WORKFLOWS.md +163 -0
  53. package/snippets/Media/Image-Element-Audit.js +350 -0
  54. package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
  55. package/snippets/Media/Video-Element-Audit.js +282 -0
  56. package/snippets/Media/WORKFLOWS.md +190 -0
  57. package/snippets/README.md +98 -0
  58. package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
  59. package/snippets/Resources/WORKFLOWS.md +173 -0
  60. package/src/bin.js +229 -0
  61. package/src/decision-tree.js +19 -0
  62. package/src/load-snippet.js +23 -0
  63. package/src/reporters/human.js +179 -0
  64. package/src/reporters/json.js +3 -0
  65. package/src/runner.js +61 -0
  66. package/src/workflows/audit.js +21 -0
  67. package/src/workflows/cwv.js +10 -0
@@ -0,0 +1,192 @@
1
+ // Service Worker Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (async () => {
5
+ if (!('serviceWorker' in navigator)) {
6
+ console.log(
7
+ '%cāš ļø Service Workers not supported in this browser',
8
+ 'color: #f59e0b; font-weight: bold;'
9
+ );
10
+ return;
11
+ }
12
+
13
+ const registrations = await navigator.serviceWorker.getRegistrations();
14
+ const controller = navigator.serviceWorker.controller;
15
+ const navEntry = performance.getEntriesByType('navigation')[0];
16
+ const resources = performance.getEntriesByType('resource');
17
+
18
+ // Resources intercepted by SW (workerStart > 0)
19
+ const swResources = resources.filter((r) => r.workerStart > 0);
20
+ const fromCache = swResources.filter((r) => r.transferSize === 0);
21
+ const fromNetwork = swResources.filter((r) => r.transferSize > 0);
22
+ const notIntercepted = resources.filter((r) => r.workerStart === 0);
23
+
24
+ console.group(
25
+ '%cāš™ļø Service Worker Analysis',
26
+ 'font-weight: bold; font-size: 14px;'
27
+ );
28
+
29
+ // Registrations
30
+ console.log('');
31
+ console.log('%cšŸ“‹ Registrations:', 'font-weight: bold;');
32
+
33
+ if (registrations.length === 0) {
34
+ console.log('%c āŒ No Service Workers registered', 'color: #ef4444;');
35
+ console.groupEnd();
36
+ return;
37
+ }
38
+
39
+ for (const reg of registrations) {
40
+ console.log('');
41
+ console.log(`%c Scope: ${reg.scope}`, 'font-family: monospace;');
42
+
43
+ if (reg.active) {
44
+ console.log(
45
+ `%c āœ… Active: ${reg.active.state}`,
46
+ 'color: #22c55e;'
47
+ );
48
+ console.log(` Script: ${reg.active.scriptURL}`);
49
+ }
50
+ if (reg.waiting) {
51
+ console.log(
52
+ '%c ā³ Waiting SW detected — update pending',
53
+ 'color: #f59e0b;'
54
+ );
55
+ console.log(` Script: ${reg.waiting.scriptURL}`);
56
+ console.log(
57
+ ' šŸ’” Call skipWaiting() to activate the new version'
58
+ );
59
+ }
60
+ if (reg.installing) {
61
+ console.log('%c šŸ”„ Installing...', 'color: #3b82f6;');
62
+ }
63
+
64
+ // Navigation Preload
65
+ if (reg.navigationPreload) {
66
+ try {
67
+ const preload = await reg.navigationPreload.getState();
68
+ console.log('');
69
+ console.log('%c šŸš€ Navigation Preload:', 'font-weight: bold;');
70
+ if (preload.enabled) {
71
+ console.log('%c āœ… Enabled', 'color: #22c55e;');
72
+ console.log(` Header value: "${preload.headerValue}"`);
73
+ } else {
74
+ console.log('%c āŒ Disabled', 'color: #ef4444;');
75
+ console.log(
76
+ ' šŸ’” Enable with: registration.navigationPreload.enable()'
77
+ );
78
+ }
79
+ } catch {
80
+ // Access may be restricted
81
+ }
82
+ }
83
+ }
84
+
85
+ // Controller
86
+ console.log('');
87
+ console.log('%cšŸŽ® Controller:', 'font-weight: bold;');
88
+ if (controller) {
89
+ console.log('%c āœ… Page is controlled by SW', 'color: #22c55e;');
90
+ console.log(` State: ${controller.state}`);
91
+ } else {
92
+ console.log('%c āš ļø Page is NOT controlled by SW', 'color: #f59e0b;');
93
+ console.log(' Hard reload detected or first visit. Do a normal reload.');
94
+ }
95
+
96
+ // SW Startup overhead
97
+ if (navEntry && navEntry.workerStart > 0) {
98
+ const workerStart = navEntry.workerStart - navEntry.startTime;
99
+ const fetchStart = navEntry.fetchStart - navEntry.startTime;
100
+ const swOverhead = Math.max(fetchStart - workerStart, 0);
101
+
102
+ console.log('');
103
+ console.log('%cā±ļø SW Startup Overhead:', 'font-weight: bold;');
104
+ console.log(` Worker start: ${workerStart.toFixed(1)}ms`);
105
+ console.log(` Fetch start: ${fetchStart.toFixed(1)}ms`);
106
+ console.log(` SW overhead: ${swOverhead.toFixed(1)}ms`);
107
+
108
+ if (swOverhead > 100) {
109
+ console.log(
110
+ '%c šŸ”“ High SW startup time — enable Navigation Preload',
111
+ 'color: #ef4444;'
112
+ );
113
+ } else if (swOverhead > 50) {
114
+ console.log(
115
+ '%c 🟔 Moderate SW startup time',
116
+ 'color: #f59e0b;'
117
+ );
118
+ } else {
119
+ console.log(
120
+ '%c 🟢 SW startup overhead is low',
121
+ 'color: #22c55e;'
122
+ );
123
+ }
124
+ }
125
+
126
+ // Cache hit/miss ratio
127
+ console.log('');
128
+ console.log('%cšŸ“Š Resource Cache Analysis:', 'font-weight: bold;');
129
+ console.log(` Total resources: ${resources.length}`);
130
+ console.log(` SW intercepted: ${swResources.length}`);
131
+ console.log(` Not intercepted: ${notIntercepted.length}`);
132
+
133
+ if (swResources.length > 0) {
134
+ const hitRate = ((fromCache.length / swResources.length) * 100).toFixed(1);
135
+ const savedKB = (
136
+ fromCache.reduce((sum, r) => sum + (r.encodedBodySize || 0), 0) / 1024
137
+ ).toFixed(1);
138
+
139
+ console.log('');
140
+ console.log(' SW-intercepted breakdown:');
141
+ console.log(` ā”œā”€ Served from cache: ${fromCache.length} (${hitRate}% hit rate)`);
142
+ console.log(` └─ Fetched from network: ${fromNetwork.length}`);
143
+ console.log(` Network bytes saved: ~${savedKB} KB`);
144
+
145
+ const rate = parseFloat(hitRate);
146
+ if (rate >= 80) {
147
+ console.log('%c 🟢 Excellent cache hit rate', 'color: #22c55e;');
148
+ } else if (rate >= 50) {
149
+ console.log('%c 🟔 Good cache hit rate', 'color: #f59e0b;');
150
+ } else {
151
+ console.log(
152
+ '%c šŸ”“ Low cache hit rate — review caching strategy',
153
+ 'color: #ef4444;'
154
+ );
155
+ }
156
+
157
+ console.log('');
158
+ console.log('%cšŸ“‹ SW-intercepted Resources (top 20):', 'font-weight: bold;');
159
+ console.table(
160
+ swResources.slice(0, 20).map((r) => ({
161
+ 'Cache': r.transferSize === 0 ? 'āœ… Cache' : '🌐 Network',
162
+ 'Transfer (KB)': r.transferSize > 0 ? (r.transferSize / 1024).toFixed(1) : '0',
163
+ 'Duration (ms)': r.duration.toFixed(0),
164
+ 'Type': r.initiatorType,
165
+ 'URL': r.name.length > 60 ? '...' + r.name.slice(-57) : r.name,
166
+ }))
167
+ );
168
+ }
169
+
170
+ // Cache Storage inventory
171
+ if ('caches' in window) {
172
+ try {
173
+ const cacheNames = await caches.keys();
174
+ if (cacheNames.length > 0) {
175
+ console.log('');
176
+ console.log('%cšŸ’¾ Cache Storage:', 'font-weight: bold;');
177
+ let totalEntries = 0;
178
+ for (const name of cacheNames) {
179
+ const cache = await caches.open(name);
180
+ const keys = await cache.keys();
181
+ totalEntries += keys.length;
182
+ console.log(` ā”œā”€ "${name}": ${keys.length} entries`);
183
+ }
184
+ console.log(` Total entries: ${totalEntries}`);
185
+ }
186
+ } catch {
187
+ // Cross-origin restrictions may prevent cache access
188
+ }
189
+ }
190
+
191
+ console.groupEnd();
192
+ })();
@@ -0,0 +1,115 @@
1
+ // Measure TTFB for all resources with sorting and summary
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ new PerformanceObserver((entryList) => {
6
+ const entries = entryList.getEntries();
7
+
8
+ const resourcesData = entries
9
+ .filter((entry) => entry.responseStart > 0)
10
+ .map((entry) => {
11
+ const url = new URL(entry.name);
12
+ const isThirdParty = url.hostname !== location.hostname;
13
+
14
+ return {
15
+ ttfb: entry.responseStart,
16
+ duration: entry.duration,
17
+ type: entry.initiatorType,
18
+ thirdParty: isThirdParty,
19
+ resource: entry.name.length > 70 ? "..." + entry.name.slice(-67) : entry.name,
20
+ fullUrl: entry.name,
21
+ };
22
+ })
23
+ .sort((a, b) => b.ttfb - a.ttfb);
24
+
25
+ if (resourcesData.length === 0) {
26
+ console.log("%cāš ļø No resources with TTFB data available", "color: #f59e0b;");
27
+ console.log("Resources may be cached or missing Timing-Allow-Origin header.");
28
+ return;
29
+ }
30
+
31
+ // Summary statistics
32
+ const ttfbValues = resourcesData.map((r) => r.ttfb);
33
+ const avgTtfb = ttfbValues.reduce((a, b) => a + b, 0) / ttfbValues.length;
34
+ const maxTtfb = Math.max(...ttfbValues);
35
+ const minTtfb = Math.min(...ttfbValues);
36
+ const thirdPartyCount = resourcesData.filter((r) => r.thirdParty).length;
37
+ const slowResources = resourcesData.filter((r) => r.ttfb > 500).length;
38
+
39
+ console.group(`%cšŸ“Š Resource TTFB Analysis (${resourcesData.length} resources)`, "font-weight: bold; font-size: 14px;");
40
+
41
+ // Summary
42
+ console.log("");
43
+ console.log("%cSummary:", "font-weight: bold;");
44
+ console.log(` Average TTFB: ${avgTtfb.toFixed(0)}ms`);
45
+ console.log(` Fastest: ${minTtfb.toFixed(0)}ms | Slowest: ${maxTtfb.toFixed(0)}ms`);
46
+ console.log(` Third-party resources: ${thirdPartyCount}`);
47
+ if (slowResources > 0) {
48
+ console.log(`%c āš ļø Slow resources (>500ms): ${slowResources}`, "color: #f59e0b;");
49
+ }
50
+
51
+ // Table (sorted by TTFB, slowest first)
52
+ console.log("");
53
+ console.log("%cResources (sorted by TTFB, slowest first):", "font-weight: bold;");
54
+ const tableData = resourcesData.slice(0, 25).map((resource) => ({
55
+ "TTFB (ms)": resource.ttfb.toFixed(0),
56
+ "Duration (ms)": resource.duration.toFixed(0),
57
+ Type: resource.type,
58
+ "3rd Party": resource.thirdParty ? "Yes" : "",
59
+ Resource: resource.resource,
60
+ }));
61
+ console.table(tableData);
62
+
63
+ if (resourcesData.length > 25) {
64
+ console.log(`... and ${resourcesData.length - 25} more resources`);
65
+ }
66
+
67
+ // Slowest resources highlight
68
+ const slowest = resourcesData.slice(0, 5);
69
+ if (slowest[0].ttfb > 500) {
70
+ console.log("");
71
+ console.log("%c🐌 Slowest resources:", "color: #ef4444; font-weight: bold;");
72
+ slowest.forEach((r, i) => {
73
+ const marker = r.thirdParty ? " [3rd party]" : "";
74
+ console.log(` ${i + 1}. ${r.ttfb.toFixed(0)}ms - ${r.type}${marker}: ${r.resource}`);
75
+ });
76
+ }
77
+
78
+ console.groupEnd();
79
+ }).observe({
80
+ type: "resource",
81
+ buffered: true,
82
+ });
83
+
84
+ // Synchronous return for agent
85
+ const resourcesSync = performance.getEntriesByType("resource")
86
+ .filter((entry) => entry.responseStart > 0)
87
+ .map((entry) => {
88
+ const url = new URL(entry.name);
89
+ return {
90
+ url: entry.name,
91
+ ttfbMs: Math.round(entry.responseStart),
92
+ durationMs: Math.round(entry.duration),
93
+ type: entry.initiatorType,
94
+ isThirdParty: url.hostname !== location.hostname,
95
+ };
96
+ })
97
+ .sort((a, b) => b.ttfbMs - a.ttfbMs);
98
+ if (resourcesSync.length === 0) {
99
+ return { script: "TTFB-Resources", status: "error", error: "No resources with TTFB data available" };
100
+ }
101
+ const ttfbVals = resourcesSync.map((r) => r.ttfbMs);
102
+ return {
103
+ script: "TTFB-Resources",
104
+ status: "ok",
105
+ count: resourcesSync.length,
106
+ details: {
107
+ avgTtfbMs: Math.round(ttfbVals.reduce((a, b) => a + b, 0) / ttfbVals.length),
108
+ maxTtfbMs: Math.max(...ttfbVals),
109
+ minTtfbMs: Math.min(...ttfbVals),
110
+ thirdPartyCount: resourcesSync.filter((r) => r.isThirdParty).length,
111
+ slowCount: resourcesSync.filter((r) => r.ttfbMs > 500).length,
112
+ },
113
+ items: resourcesSync,
114
+ };
115
+ })();
@@ -0,0 +1,117 @@
1
+ // Measure TTFB sub-parts breakdown
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ new PerformanceObserver((entryList) => {
6
+ const [pageNav] = entryList.getEntriesByType("navigation");
7
+
8
+ const activationStart = pageNav.activationStart || 0;
9
+ const waitEnd = Math.max((pageNav.workerStart || pageNav.fetchStart) - activationStart, 0);
10
+ const dnsStart = Math.max(pageNav.domainLookupStart - activationStart, 0);
11
+ const tcpStart = Math.max(pageNav.connectStart - activationStart, 0);
12
+ const sslStart = Math.max(pageNav.secureConnectionStart - activationStart, 0);
13
+ const tcpEnd = Math.max(pageNav.connectEnd - activationStart, 0);
14
+ const responseStart = Math.max(pageNav.responseStart - activationStart, 0);
15
+
16
+ const formatMs = (ms) => ms.toFixed(2) + " ms";
17
+ const formatBar = (ms, total) => {
18
+ const pct = total > 0 ? (ms / total) * 100 : 0;
19
+ const width = Math.round(pct / 5);
20
+ return "ā–ˆ".repeat(width) + "ā–‘".repeat(20 - width) + ` ${pct.toFixed(1)}%`;
21
+ };
22
+
23
+ // Rating
24
+ let rating, color;
25
+ if (responseStart <= 800) {
26
+ rating = "Good";
27
+ color = "#22c55e";
28
+ } else if (responseStart <= 1800) {
29
+ rating = "Needs Improvement";
30
+ color = "#f59e0b";
31
+ } else {
32
+ rating = "Poor";
33
+ color = "#ef4444";
34
+ }
35
+
36
+ console.group(`%cā±ļø TTFB: ${responseStart.toFixed(0)}ms (${rating})`, `color: ${color}; font-weight: bold; font-size: 14px;`);
37
+
38
+ // Sub-parts breakdown
39
+ const subParts = [
40
+ { name: "Redirect/Wait", duration: waitEnd, icon: "šŸ”„" },
41
+ { name: "Service Worker/Cache", duration: dnsStart - waitEnd, icon: "āš™ļø" },
42
+ { name: "DNS Lookup", duration: tcpStart - dnsStart, icon: "šŸ”" },
43
+ { name: "TCP Connection", duration: sslStart - tcpStart, icon: "šŸ”Œ" },
44
+ { name: "SSL/TLS", duration: tcpEnd - sslStart, icon: "šŸ”’" },
45
+ { name: "Server Response", duration: responseStart - tcpEnd, icon: "šŸ“„" },
46
+ ];
47
+
48
+ console.log("");
49
+ console.log("%cBreakdown:", "font-weight: bold;");
50
+ subParts.forEach(({ name, duration, icon }) => {
51
+ if (duration > 0) {
52
+ console.log(`${icon} ${name.padEnd(20)} ${formatMs(duration).padStart(10)} ${formatBar(duration, responseStart)}`);
53
+ }
54
+ });
55
+
56
+ console.log("");
57
+ console.log(`%c${"─".repeat(60)}`, "color: #666;");
58
+ console.log(`%cšŸ“Š Total TTFB${" ".repeat(15)} ${formatMs(responseStart).padStart(10)}`, "font-weight: bold;");
59
+
60
+ // Recommendations based on longest phase
61
+ const longestPhase = subParts.reduce((a, b) => (a.duration > b.duration ? a : b));
62
+ if (longestPhase.duration > responseStart * 0.4 && responseStart > 800) {
63
+ console.log("");
64
+ console.log("%cšŸ’” Recommendation:", "color: #3b82f6; font-weight: bold;");
65
+ const tips = {
66
+ "Redirect/Wait": "Minimize redirects. Use direct URLs where possible.",
67
+ "Service Worker/Cache": "Optimize service worker. Consider cache-first strategies.",
68
+ "DNS Lookup": "Use DNS prefetching. Consider a faster DNS provider.",
69
+ "TCP Connection": "Enable HTTP/2 or HTTP/3. Use connection keep-alive.",
70
+ "SSL/TLS": "Enable TLS 1.3. Use session resumption. Check certificate chain.",
71
+ "Server Response": "Optimize server processing. Add caching. Check database queries.",
72
+ };
73
+ console.log(` ${longestPhase.name} is ${((longestPhase.duration / responseStart) * 100).toFixed(0)}% of TTFB.`);
74
+ console.log(` ${tips[longestPhase.name]}`);
75
+ }
76
+
77
+ console.groupEnd();
78
+ }).observe({
79
+ type: "navigation",
80
+ buffered: true,
81
+ });
82
+
83
+ // Synchronous return for agent
84
+ const [navSync] = performance.getEntriesByType("navigation");
85
+ if (!navSync) return { script: "TTFB-Sub-Parts", status: "error", error: "No navigation entry" };
86
+ const activStartSync = navSync.activationStart || 0;
87
+ const ttfbValueSync = Math.max(0, navSync.responseStart - activStartSync);
88
+ const value = Math.round(ttfbValueSync);
89
+ const rating = value <= 800 ? "good" : value <= 1800 ? "needs-improvement" : "poor";
90
+ const waitEndSync = Math.max((navSync.workerStart || navSync.fetchStart) - activStartSync, 0);
91
+ const dnsStartSync = Math.max(navSync.domainLookupStart - activStartSync, 0);
92
+ const tcpStartSync = Math.max(navSync.connectStart - activStartSync, 0);
93
+ const sslStartSync = Math.max(navSync.secureConnectionStart - activStartSync, 0);
94
+ const tcpEndSync = Math.max(navSync.connectEnd - activStartSync, 0);
95
+ const subPartsSync = [
96
+ { key: "redirectWait", duration: waitEndSync },
97
+ { key: "serviceWorkerCache", duration: dnsStartSync - waitEndSync },
98
+ { key: "dnsLookup", duration: tcpStartSync - dnsStartSync },
99
+ { key: "tcpConnection", duration: sslStartSync - tcpStartSync },
100
+ { key: "sslTls", duration: tcpEndSync - sslStartSync },
101
+ { key: "serverResponse", duration: ttfbValueSync - tcpEndSync },
102
+ ];
103
+ const slowestSync = subPartsSync.reduce((a, b) => a.duration > b.duration ? a : b);
104
+ return {
105
+ script: "TTFB-Sub-Parts",
106
+ status: "ok",
107
+ metric: "TTFB",
108
+ value,
109
+ unit: "ms",
110
+ rating,
111
+ thresholds: { good: 800, needsImprovement: 1800 },
112
+ details: {
113
+ subParts: Object.fromEntries(subPartsSync.map((p) => [p.key, { value: Math.round(p.duration), unit: "ms" }])),
114
+ slowestPhase: slowestSync.key,
115
+ },
116
+ };
117
+ })();
@@ -0,0 +1,37 @@
1
+ // Measure TTFB with threshold indicator
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ new PerformanceObserver((entryList) => {
6
+ const [pageNav] = entryList.getEntriesByType("navigation");
7
+ const ttfb = pageNav.responseStart;
8
+
9
+ let rating, color;
10
+ if (ttfb <= 800) {
11
+ rating = "Good";
12
+ color = "#22c55e";
13
+ } else if (ttfb <= 1800) {
14
+ rating = "Needs Improvement";
15
+ color = "#f59e0b";
16
+ } else {
17
+ rating = "Poor";
18
+ color = "#ef4444";
19
+ }
20
+
21
+ console.log(
22
+ `%cTTFB: ${ttfb.toFixed(0)}ms (${rating})`,
23
+ `color: ${color}; font-weight: bold; font-size: 14px;`
24
+ );
25
+ }).observe({
26
+ type: "navigation",
27
+ buffered: true,
28
+ });
29
+
30
+ // Synchronous return for agent
31
+ const [nav] = performance.getEntriesByType("navigation");
32
+ if (!nav) return { script: "TTFB", status: "error", error: "No navigation entry" };
33
+ const value = Math.round(nav.responseStart);
34
+ const rating = value <= 800 ? "good" : value <= 1800 ? "needs-improvement" : "poor";
35
+ return { script: "TTFB", status: "ok", metric: "TTFB", value, unit: "ms", rating,
36
+ thresholds: { good: 800, needsImprovement: 1800 } };
37
+ })();