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.
- package/README.md +139 -0
- package/package.json +54 -0
- package/snippets/CoreWebVitals/CLS.js +88 -0
- package/snippets/CoreWebVitals/INP.js +432 -0
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
- package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
- package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
- package/snippets/CoreWebVitals/LCP.js +135 -0
- package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
- package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
- package/snippets/Interaction/Interactions.js +220 -0
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
- package/snippets/Interaction/Long-Animation-Frames.js +298 -0
- package/snippets/Interaction/LongTask.js +203 -0
- package/snippets/Interaction/Scroll-Performance.js +359 -0
- package/snippets/Interaction/WORKFLOWS.md +205 -0
- package/snippets/Loading/Back-Forward-Cache.js +473 -0
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
- package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
- package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
- package/snippets/Loading/Content-Visibility.js +352 -0
- package/snippets/Loading/Critical-CSS-Detection.js +329 -0
- package/snippets/Loading/Event-Processing-Time.js +179 -0
- package/snippets/Loading/FCP.js +101 -0
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
- package/snippets/Loading/Find-render-blocking-resources.js +161 -0
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
- package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
- package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
- package/snippets/Loading/Priority-Hints-Audit.js +336 -0
- package/snippets/Loading/Resource-Hints-Validation.js +503 -0
- package/snippets/Loading/Resource-Hints.js +400 -0
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
- package/snippets/Loading/Script-Loading.js +332 -0
- package/snippets/Loading/Service-Worker-Analysis.js +192 -0
- package/snippets/Loading/TTFB-Resources.js +115 -0
- package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
- package/snippets/Loading/TTFB.js +37 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
- package/snippets/Loading/WORKFLOWS.md +163 -0
- package/snippets/Media/Image-Element-Audit.js +350 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
- package/snippets/Media/Video-Element-Audit.js +282 -0
- package/snippets/Media/WORKFLOWS.md +190 -0
- package/snippets/README.md +98 -0
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
- package/snippets/Resources/WORKFLOWS.md +173 -0
- package/src/bin.js +229 -0
- package/src/decision-tree.js +19 -0
- package/src/load-snippet.js +23 -0
- package/src/reporters/human.js +179 -0
- package/src/reporters/json.js +3 -0
- package/src/runner.js +61 -0
- package/src/workflows/audit.js +21 -0
- 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
|
+
})();
|