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,137 @@
1
+ // Long Animation Frames - Script Attribution Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ // Check browser support
6
+ if (!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) {
7
+ console.log("%c⚠️ Long Animation Frames not supported", "color: #f59e0b; font-weight: bold;");
8
+ console.log("Chrome 116+ required.");
9
+ return { script: "Long-Animation-Frames-Script-Attribution", status: "unsupported", error: "long-animation-frame not supported. Chrome 116+ required." };
10
+ }
11
+
12
+ // Read buffered LoAF entries immediately
13
+ const frames = performance.getEntriesByType("long-animation-frame").map((entry) => ({
14
+ duration: entry.duration,
15
+ blockingDuration: entry.blockingDuration || 0,
16
+ scripts: (entry.scripts || []).map((s) => ({
17
+ sourceURL: s.sourceURL || "unknown",
18
+ sourceFunctionName: s.sourceFunctionName || "anonymous",
19
+ invoker: s.invoker || "unknown",
20
+ duration: s.duration || 0,
21
+ })),
22
+ }));
23
+
24
+ if (frames.length === 0) {
25
+ console.log("%c✅ No long frames detected", "color: #22c55e; font-weight: bold;");
26
+ return { script: "Long-Animation-Frames-Script-Attribution", status: "ok",
27
+ details: { frameCount: 0, totalBlockingMs: 0, byCategory: {} }, items: [] };
28
+ }
29
+
30
+ const allScripts = frames.flatMap((f) => f.scripts);
31
+ const totalBlocking = frames.reduce((sum, f) => sum + f.blockingDuration, 0);
32
+ const totalDuration = frames.reduce((sum, f) => sum + f.duration, 0);
33
+
34
+ const categorize = (url) => {
35
+ const u = url.toLowerCase();
36
+ const origin = location.origin.toLowerCase();
37
+ if (u === "" || u === "unknown") return "unknown";
38
+ if (["react", "vue", "angular", "svelte", "framework", "chunk", "webpack", "vite"].some((fw) => u.includes(fw))) return "framework";
39
+ if (["google-analytics", "gtag", "gtm", "analytics", "facebook", "twitter", "ads", "cdn", "unpkg", "jsdelivr", "segment", "amplitude"].some((tp) => u.includes(tp))) return "third-party";
40
+ if (u.startsWith("chrome-extension://") || u.startsWith("moz-extension://")) return "extension";
41
+ if (u.startsWith("http") && !u.includes(origin.replace(/^https?:\/\//, ""))) return "third-party";
42
+ return "first-party";
43
+ };
44
+
45
+ const byCategory = {};
46
+ allScripts.forEach((script) => {
47
+ const cat = categorize(script.sourceURL);
48
+ if (!byCategory[cat]) byCategory[cat] = { durationMs: 0, count: 0 };
49
+ byCategory[cat].durationMs += script.duration;
50
+ byCategory[cat].count++;
51
+ });
52
+
53
+ const byFile = {};
54
+ allScripts.forEach((script) => {
55
+ const file = script.sourceURL.split("/").pop() || "unknown";
56
+ if (!byFile[file]) byFile[file] = { duration: 0, count: 0, category: categorize(script.sourceURL), functions: [] };
57
+ byFile[file].duration += script.duration;
58
+ byFile[file].count++;
59
+ byFile[file].functions.push({ name: script.sourceFunctionName, invoker: script.invoker, duration: script.duration });
60
+ });
61
+
62
+ const topFiles = Object.entries(byFile).sort((a, b) => b[1].duration - a[1].duration).slice(0, 10);
63
+ const totalScript = Object.values(byCategory).reduce((sum, cat) => sum + cat.durationMs, 0);
64
+
65
+ const categories = [
66
+ { name: "Your Code", key: "first-party", icon: "🔵", color: "#3b82f6" },
67
+ { name: "Framework", key: "framework", icon: "🟣", color: "#8b5cf6" },
68
+ { name: "Third-Party", key: "third-party", icon: "🟠", color: "#f59e0b" },
69
+ { name: "Extensions", key: "extension", icon: "🟤", color: "#92400e" },
70
+ { name: "Unknown", key: "unknown", icon: "⚫", color: "#6b7280" },
71
+ ];
72
+
73
+ console.log("%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "color: #6b7280;");
74
+ console.log("%c📊 Blocking Time by Category", "font-weight: bold; font-size: 13px;");
75
+ console.log("");
76
+
77
+ categories.forEach(({ name, key, icon, color }) => {
78
+ const data = byCategory[key];
79
+ if (data?.durationMs > 0) {
80
+ const pct = totalScript > 0 ? (data.durationMs / totalScript) * 100 : 0;
81
+ const barLen = Math.round(pct / 2);
82
+ const bar = "█".repeat(barLen) + "░".repeat(Math.max(0, 50 - barLen));
83
+ console.log(
84
+ `%c${icon} ${name.padEnd(12)} %c${bar} %c${pct.toFixed(1)}% %c(${data.durationMs.toFixed(0)}ms, ${data.count} scripts)`,
85
+ `color: ${color}; font-weight: bold;`, `color: ${color};`, `color: ${color}; font-weight: bold;`, "color: #6b7280;",
86
+ );
87
+ }
88
+ });
89
+
90
+ console.log("");
91
+ console.log(`%cTotal Script: ${totalScript.toFixed(0)}ms | Blocking: ${totalBlocking.toFixed(0)}ms | Duration: ${totalDuration.toFixed(0)}ms`, "color: #6b7280;");
92
+ console.log(`%cFrames captured: ${frames.length}`, "color: #6b7280;");
93
+
94
+ console.log("");
95
+ console.log("%c━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "color: #6b7280;");
96
+ console.log("%c🔥 Top 10 Blocking Scripts", "font-weight: bold; font-size: 13px;");
97
+ console.log("");
98
+
99
+ topFiles.forEach(([file, data], idx) => {
100
+ const pct = totalScript > 0 ? (data.duration / totalScript) * 100 : 0;
101
+ const icon = categories.find((c) => c.key === data.category)?.icon || "⚫";
102
+ console.group(
103
+ `%c${idx + 1}. ${icon} ${file} %c(${data.duration.toFixed(0)}ms, ${pct.toFixed(1)}%)`,
104
+ "font-weight: bold;", "color: #6b7280;",
105
+ );
106
+ console.log(`Category: ${data.category}`);
107
+ console.log(`Executions: ${data.count}`);
108
+ console.log(`Avg duration: ${(data.duration / data.count).toFixed(0)}ms`);
109
+ const topFns = data.functions.sort((a, b) => b.duration - a.duration).slice(0, 3);
110
+ if (topFns.length > 0) {
111
+ console.log("Top functions:");
112
+ topFns.forEach((fn, i) => console.log(` ${i + 1}. ${fn.name} (${fn.duration.toFixed(0)}ms) - ${fn.invoker}`));
113
+ }
114
+ console.groupEnd();
115
+ });
116
+
117
+ console.log("");
118
+ console.log("%c✅ Analysis complete!", "color: #22c55e; font-weight: bold;");
119
+
120
+ return {
121
+ script: "Long-Animation-Frames-Script-Attribution",
122
+ status: "ok",
123
+ details: {
124
+ frameCount: frames.length,
125
+ totalBlockingMs: Math.round(totalBlocking),
126
+ byCategory: Object.fromEntries(
127
+ Object.entries(byCategory).map(([k, v]) => [k, { durationMs: Math.round(v.durationMs), count: v.count }])
128
+ ),
129
+ },
130
+ items: topFiles.map(([file, data]) => ({
131
+ file,
132
+ category: data.category,
133
+ durationMs: Math.round(data.duration),
134
+ count: data.count,
135
+ })),
136
+ };
137
+ })();
@@ -0,0 +1,298 @@
1
+ // Long Animation Frames Analysis
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ const formatMs = (ms) => `${Math.round(ms)}ms`;
6
+
7
+ // Rating based on blocking duration
8
+ const valueToRating = (blockingDuration) =>
9
+ blockingDuration === 0 ? "good" : blockingDuration <= 100 ? "needs-improvement" : "poor";
10
+
11
+ const RATING_COLORS = {
12
+ good: "#0CCE6A",
13
+ "needs-improvement": "#FFA400",
14
+ poor: "#FF4E42",
15
+ };
16
+
17
+ const RATING_ICONS = {
18
+ good: "🟢",
19
+ "needs-improvement": "🟡",
20
+ poor: "🔴",
21
+ };
22
+
23
+ // Track all LoAFs and events
24
+ const allLoAFs = [];
25
+ const allEvents = [];
26
+
27
+ const getScriptSummary = (script) => {
28
+ const invoker = script.invoker || script.name || "(anonymous)";
29
+ const source = script.sourceURL
30
+ ? script.sourceURL.split("/").pop()?.split("?")[0] || script.sourceURL
31
+ : "";
32
+ return { invoker, source, type: script.invokerType || "unknown" };
33
+ };
34
+
35
+ const processLoAF = (entry) => {
36
+ const endTime = entry.startTime + entry.duration;
37
+
38
+ // Calculate derived metrics
39
+ const workDuration = entry.renderStart
40
+ ? entry.renderStart - entry.startTime
41
+ : entry.duration;
42
+
43
+ const renderDuration = entry.renderStart
44
+ ? endTime - entry.renderStart
45
+ : 0;
46
+
47
+ const styleAndLayoutDuration = entry.styleAndLayoutStart
48
+ ? endTime - entry.styleAndLayoutStart
49
+ : 0;
50
+
51
+ const totalForcedStyleAndLayout = entry.scripts.reduce(
52
+ (sum, script) => sum + (script.forcedStyleAndLayoutDuration || 0),
53
+ 0
54
+ );
55
+
56
+ // Process scripts
57
+ const scripts = entry.scripts.map((script) => ({
58
+ ...getScriptSummary(script),
59
+ duration: Math.round(script.duration),
60
+ execDuration: Math.round(script.executionStart
61
+ ? script.startTime + script.duration - script.executionStart
62
+ : script.duration),
63
+ forcedStyleAndLayout: Math.round(script.forcedStyleAndLayoutDuration || 0),
64
+ startTime: Math.round(script.startTime),
65
+ }));
66
+
67
+ return {
68
+ startTime: Math.round(entry.startTime),
69
+ duration: Math.round(entry.duration),
70
+ blockingDuration: Math.round(entry.blockingDuration),
71
+ workDuration: Math.round(workDuration),
72
+ renderDuration: Math.round(renderDuration),
73
+ styleAndLayoutDuration: Math.round(styleAndLayoutDuration),
74
+ totalForcedStyleAndLayout: Math.round(totalForcedStyleAndLayout),
75
+ scripts,
76
+ entry,
77
+ };
78
+ };
79
+
80
+ const overlap = (e1, e2) =>
81
+ e1.startTime < e2.startTime + e2.duration &&
82
+ e2.startTime < e1.startTime + e1.duration;
83
+
84
+ // LoAF Observer
85
+ const loafObserver = new PerformanceObserver((list) => {
86
+ for (const entry of list.getEntries()) {
87
+ const processed = processLoAF(entry);
88
+ allLoAFs.push(processed);
89
+
90
+ // Only log frames with blocking duration
91
+ if (entry.blockingDuration > 0) {
92
+ const rating = valueToRating(entry.blockingDuration);
93
+ const icon = RATING_ICONS[rating];
94
+ const color = RATING_COLORS[rating];
95
+
96
+ console.groupCollapsed(
97
+ `%c${icon} Long Animation Frame: ${formatMs(entry.duration)} (blocking: ${formatMs(entry.blockingDuration)})`,
98
+ `font-weight: bold; color: ${color};`
99
+ );
100
+
101
+ // Time breakdown
102
+ console.log("%cTime Breakdown:", "font-weight: bold;");
103
+ const breakdown = [
104
+ { Phase: "Work (JS)", Duration: formatMs(processed.workDuration) },
105
+ { Phase: "Render", Duration: formatMs(processed.renderDuration) },
106
+ { Phase: "Style & Layout", Duration: formatMs(processed.styleAndLayoutDuration) },
107
+ ];
108
+ console.table(breakdown);
109
+
110
+ // Visual bar
111
+ const total = processed.duration;
112
+ const barWidth = 40;
113
+ const workBar = "█".repeat(Math.round((processed.workDuration / total) * barWidth));
114
+ const renderBar = "░".repeat(Math.round((processed.renderDuration / total) * barWidth));
115
+ console.log(` ${workBar}${renderBar}`);
116
+ console.log(" █ Work ░ Render");
117
+
118
+ // Forced style/layout warning
119
+ if (processed.totalForcedStyleAndLayout > 0) {
120
+ console.log("");
121
+ console.log(
122
+ `%c⚠️ Forced style/layout: ${formatMs(processed.totalForcedStyleAndLayout)}`,
123
+ "color: #ef4444; font-weight: bold;"
124
+ );
125
+ }
126
+
127
+ // Scripts
128
+ if (processed.scripts.length > 0) {
129
+ console.log("");
130
+ console.log("%cScripts:", "font-weight: bold;");
131
+
132
+ const scriptTable = processed.scripts.map((s) => ({
133
+ Invoker: s.invoker.length > 40 ? s.invoker.slice(0, 37) + "..." : s.invoker,
134
+ Type: s.type,
135
+ Duration: formatMs(s.duration),
136
+ "Forced S&L": s.forcedStyleAndLayout > 0 ? formatMs(s.forcedStyleAndLayout) : "-",
137
+ Source: s.source.length > 25 ? "..." + s.source.slice(-22) : s.source,
138
+ }));
139
+ console.table(scriptTable);
140
+ }
141
+
142
+ // Find overlapping events (interactions during this frame)
143
+ const overlappingEvents = allEvents.filter((e) => overlap(e, entry));
144
+ if (overlappingEvents.length > 0) {
145
+ console.log("");
146
+ console.log("%c👆 Interactions during this frame:", "font-weight: bold;");
147
+ overlappingEvents.forEach((e) => {
148
+ console.log(` ${e.name}: ${formatMs(e.duration)}`);
149
+ });
150
+ }
151
+
152
+ console.groupEnd();
153
+ }
154
+ }
155
+ });
156
+
157
+ // Event Observer (for correlation)
158
+ const eventObserver = new PerformanceObserver((list) => {
159
+ for (const entry of list.getEntries()) {
160
+ if (entry.interactionId) {
161
+ allEvents.push(entry);
162
+ }
163
+ }
164
+ });
165
+
166
+ loafObserver.observe({ type: "long-animation-frame", buffered: true });
167
+ eventObserver.observe({ type: "event", buffered: true });
168
+
169
+ // Summary function
170
+ window.getLoAFSummary = () => {
171
+ console.group("%c📊 Long Animation Frames Summary", "font-weight: bold; font-size: 14px;");
172
+
173
+ if (allLoAFs.length === 0) {
174
+ console.log(" No long animation frames recorded.");
175
+ console.groupEnd();
176
+ return;
177
+ }
178
+
179
+ const blocking = allLoAFs.filter((l) => l.blockingDuration > 0);
180
+ const totalBlocking = blocking.reduce((sum, l) => sum + l.blockingDuration, 0);
181
+ const worstBlocking = Math.max(...allLoAFs.map((l) => l.blockingDuration));
182
+ const avgDuration = allLoAFs.reduce((sum, l) => sum + l.duration, 0) / allLoAFs.length;
183
+
184
+ // Statistics
185
+ console.log("");
186
+ console.log("%cStatistics:", "font-weight: bold;");
187
+ console.log(` Total LoAFs: ${allLoAFs.length}`);
188
+ console.log(` With blocking time: ${blocking.length}`);
189
+ console.log(` Total blocking time: ${formatMs(totalBlocking)}`);
190
+ console.log(` Worst blocking: ${formatMs(worstBlocking)}`);
191
+ console.log(` Average duration: ${formatMs(avgDuration)}`);
192
+
193
+ // Script analysis
194
+ const scriptStats = new Map();
195
+ allLoAFs.forEach((loaf) => {
196
+ loaf.scripts.forEach((script) => {
197
+ const key = `${script.invoker}|${script.source}`;
198
+ if (!scriptStats.has(key)) {
199
+ scriptStats.set(key, {
200
+ invoker: script.invoker,
201
+ source: script.source,
202
+ count: 0,
203
+ totalDuration: 0,
204
+ totalForcedSL: 0,
205
+ });
206
+ }
207
+ const stats = scriptStats.get(key);
208
+ stats.count++;
209
+ stats.totalDuration += script.duration;
210
+ stats.totalForcedSL += script.forcedStyleAndLayout;
211
+ });
212
+ });
213
+
214
+ if (scriptStats.size > 0) {
215
+ console.log("");
216
+ console.log("%c🎯 Top Scripts by Total Duration:", "font-weight: bold; color: #ef4444;");
217
+
218
+ const topScripts = Array.from(scriptStats.values())
219
+ .sort((a, b) => b.totalDuration - a.totalDuration)
220
+ .slice(0, 10);
221
+
222
+ const scriptTable = topScripts.map((s) => ({
223
+ Invoker: s.invoker.length > 35 ? s.invoker.slice(0, 32) + "..." : s.invoker,
224
+ Count: s.count,
225
+ "Total Duration": formatMs(s.totalDuration),
226
+ "Forced S&L": s.totalForcedSL > 0 ? formatMs(s.totalForcedSL) : "-",
227
+ Source: s.source.length > 20 ? "..." + s.source.slice(-17) : s.source,
228
+ }));
229
+ console.table(scriptTable);
230
+ }
231
+
232
+ // Forced style/layout analysis
233
+ const forcedSLTotal = allLoAFs.reduce((sum, l) => sum + l.totalForcedStyleAndLayout, 0);
234
+ if (forcedSLTotal > 0) {
235
+ console.log("");
236
+ console.log(
237
+ `%c⚠️ Total forced style/layout: ${formatMs(forcedSLTotal)}`,
238
+ "color: #ef4444; font-weight: bold;"
239
+ );
240
+ console.log(" This indicates layout thrashing - reading layout after writing to DOM.");
241
+ }
242
+
243
+ // Recommendations
244
+ if (worstBlocking > 50) {
245
+ console.log("");
246
+ console.log("%c💡 Recommendations:", "font-weight: bold; color: #3b82f6;");
247
+ console.log(" • Break up long tasks using scheduler.yield() or setTimeout");
248
+ console.log(" • Move heavy computation to Web Workers");
249
+ console.log(" • Avoid forced synchronous layouts (read before write)");
250
+ console.log(" • Defer non-critical work with requestIdleCallback");
251
+ }
252
+
253
+ console.groupEnd();
254
+
255
+ return {
256
+ script: "Long-Animation-Frames",
257
+ status: "ok",
258
+ count: allLoAFs.length,
259
+ details: {
260
+ totalLoAFs: allLoAFs.length,
261
+ withBlockingTime: blocking.length,
262
+ totalBlockingTimeMs: Math.round(totalBlocking),
263
+ worstBlockingMs: Math.round(worstBlocking),
264
+ topScripts: Array.from(scriptStats.values())
265
+ .sort((a, b) => b.totalDuration - a.totalDuration)
266
+ .slice(0, 5)
267
+ .map((s) => ({ invoker: s.invoker, source: s.source, totalDurationMs: Math.round(s.totalDuration), count: s.count })),
268
+ },
269
+ };
270
+ };
271
+
272
+ console.log("%c🎬 Long Animation Frames Tracking Active", "font-weight: bold; font-size: 14px;");
273
+ console.log(" Frames with blocking duration will be logged.");
274
+ console.log(
275
+ " Call %cgetLoAFSummary()%c for full analysis.",
276
+ "font-family: monospace; background: #f3f4f6; padding: 2px 4px;",
277
+ ""
278
+ );
279
+
280
+ // Synchronous return for agent (buffered entries)
281
+ const loafBuffered = performance.getEntriesByType("long-animation-frame");
282
+ const blockingLoafs = loafBuffered.filter((e) => e.blockingDuration > 0);
283
+ const totalBlockingSync = blockingLoafs.reduce((sum, e) => sum + e.blockingDuration, 0);
284
+ const worstBlockingSync = loafBuffered.length > 0 ? Math.max(...loafBuffered.map((e) => e.blockingDuration)) : 0;
285
+ return {
286
+ script: "Long-Animation-Frames",
287
+ status: "tracking",
288
+ count: loafBuffered.length,
289
+ details: {
290
+ totalLoAFs: loafBuffered.length,
291
+ withBlockingTime: blockingLoafs.length,
292
+ totalBlockingTimeMs: Math.round(totalBlockingSync),
293
+ worstBlockingMs: Math.round(worstBlockingSync),
294
+ },
295
+ message: "Tracking long animation frames. Call getLoAFSummary() for full script attribution.",
296
+ getDataFn: "getLoAFSummary",
297
+ };
298
+ })();
@@ -0,0 +1,203 @@
1
+ // Long Tasks Tracking
2
+ // https://webperf-snippets.nucliweb.net
3
+
4
+ (() => {
5
+ const formatMs = (ms) => `${Math.round(ms)}ms`;
6
+
7
+ // Severity helpers
8
+ const getSeverity = (duration) => {
9
+ if (duration > 250) return { level: "critical", icon: "🔴", color: "#ef4444" };
10
+ if (duration > 150) return { level: "high", icon: "🟠", color: "#f97316" };
11
+ if (duration > 100) return { level: "medium", icon: "🟡", color: "#eab308" };
12
+ return { level: "low", icon: "🟢", color: "#22c55e" };
13
+ };
14
+
15
+ // Track all long tasks
16
+ const allTasks = [];
17
+ let totalBlockingTime = 0;
18
+
19
+ try {
20
+ const observer = new PerformanceObserver((list) => {
21
+ for (const entry of list.getEntries()) {
22
+ const duration = entry.duration;
23
+ const blockingTime = Math.max(0, duration - 50); // Time over 50ms threshold
24
+ totalBlockingTime += blockingTime;
25
+
26
+ const sev = getSeverity(duration);
27
+
28
+ const task = {
29
+ startTime: entry.startTime,
30
+ duration,
31
+ blockingTime,
32
+ severity: sev.level,
33
+ attribution: entry.attribution?.[0]?.containerType || "unknown",
34
+ containerId: entry.attribution?.[0]?.containerId || "",
35
+ containerName: entry.attribution?.[0]?.containerName || "",
36
+ };
37
+ allTasks.push(task);
38
+
39
+ // Log each long task
40
+ console.groupCollapsed(
41
+ `%c${sev.icon} Long Task: ${formatMs(duration)} (blocking: ${formatMs(blockingTime)})`,
42
+ `font-weight: bold; color: ${sev.color};`
43
+ );
44
+
45
+ console.log(`%cTiming:`, "font-weight: bold;");
46
+ console.log(` Start: ${formatMs(entry.startTime)}`);
47
+ console.log(` Duration: ${formatMs(duration)}`);
48
+ console.log(` Blocking time: ${formatMs(blockingTime)}`);
49
+
50
+ // Attribution (limited in Long Tasks API)
51
+ if (entry.attribution && entry.attribution.length > 0) {
52
+ console.log("");
53
+ console.log(`%cAttribution:`, "font-weight: bold;");
54
+ entry.attribution.forEach((attr) => {
55
+ console.log(` Container: ${attr.containerType || "window"}`);
56
+ if (attr.containerName) console.log(` Name: ${attr.containerName}`);
57
+ if (attr.containerId) console.log(` ID: ${attr.containerId}`);
58
+ if (attr.containerSrc) console.log(` Source: ${attr.containerSrc}`);
59
+ });
60
+ }
61
+
62
+ // Running totals
63
+ console.log("");
64
+ console.log(`%cSession totals:`, "font-weight: bold;");
65
+ console.log(` Long tasks: ${allTasks.length}`);
66
+ console.log(` Total blocking time: ${formatMs(totalBlockingTime)}`);
67
+
68
+ console.groupEnd();
69
+ }
70
+ });
71
+
72
+ observer.observe({ type: "longtask", buffered: true });
73
+
74
+ // Summary function
75
+ window.getLongTaskSummary = () => {
76
+ console.group("%c📊 Long Tasks Summary", "font-weight: bold; font-size: 14px;");
77
+
78
+ if (allTasks.length === 0) {
79
+ console.log(" No long tasks recorded.");
80
+ console.log(" This is good! The main thread has been responsive.");
81
+ console.groupEnd();
82
+ return;
83
+ }
84
+
85
+ const durations = allTasks.map((t) => t.duration);
86
+ const worst = Math.max(...durations);
87
+ const avg = durations.reduce((a, b) => a + b, 0) / durations.length;
88
+
89
+ const worstSev = getSeverity(worst);
90
+
91
+ // Statistics
92
+ console.log("");
93
+ console.log("%cStatistics:", "font-weight: bold;");
94
+ console.log(` Total long tasks: ${allTasks.length}`);
95
+ console.log(` Total blocking time: ${formatMs(totalBlockingTime)}`);
96
+ console.log(
97
+ ` Worst task: %c${formatMs(worst)}`,
98
+ `color: ${worstSev.color}; font-weight: bold;`
99
+ );
100
+ console.log(` Average duration: ${formatMs(avg)}`);
101
+
102
+ // By severity
103
+ const bySeverity = {
104
+ critical: allTasks.filter((t) => t.severity === "critical").length,
105
+ high: allTasks.filter((t) => t.severity === "high").length,
106
+ medium: allTasks.filter((t) => t.severity === "medium").length,
107
+ low: allTasks.filter((t) => t.severity === "low").length,
108
+ };
109
+
110
+ console.log("");
111
+ console.log("%cBy severity:", "font-weight: bold;");
112
+ console.log(` 🔴 Critical (>250ms): ${bySeverity.critical}`);
113
+ console.log(` 🟠 High (150-250ms): ${bySeverity.high}`);
114
+ console.log(` 🟡 Medium (100-150ms): ${bySeverity.medium}`);
115
+ console.log(` 🟢 Low (50-100ms): ${bySeverity.low}`);
116
+
117
+ // Timeline
118
+ if (allTasks.length > 0) {
119
+ console.log("");
120
+ console.log("%c⏱️ Timeline:", "font-weight: bold;");
121
+
122
+ const tableData = allTasks
123
+ .sort((a, b) => b.duration - a.duration)
124
+ .slice(0, 10)
125
+ .map((t) => {
126
+ const sev = getSeverity(t.duration);
127
+ return {
128
+ "": sev.icon,
129
+ Start: formatMs(t.startTime),
130
+ Duration: formatMs(t.duration),
131
+ Blocking: formatMs(t.blockingTime),
132
+ Container: t.attribution,
133
+ };
134
+ });
135
+ console.table(tableData);
136
+ }
137
+
138
+ // Recommendations
139
+ if (totalBlockingTime > 300) {
140
+ console.log("");
141
+ console.log("%c💡 Recommendations:", "font-weight: bold; color: #3b82f6;");
142
+ console.log(" • Break up long tasks using setTimeout or scheduler.yield()");
143
+ console.log(" • Move heavy computation to Web Workers");
144
+ console.log(" • Defer non-critical JavaScript");
145
+ console.log(" • Use requestIdleCallback for low-priority work");
146
+ console.log("");
147
+ console.log("%c🔍 For better debugging:", "font-weight: bold;");
148
+ console.log(" Use the Long Animation Frames snippet for full script attribution");
149
+ }
150
+
151
+ console.groupEnd();
152
+
153
+ return {
154
+ script: "LongTask",
155
+ status: "ok",
156
+ count: allTasks.length,
157
+ details: {
158
+ totalBlockingTimeMs: Math.round(totalBlockingTime),
159
+ worstTaskMs: Math.round(worst),
160
+ avgDurationMs: Math.round(avg),
161
+ bySeverity,
162
+ },
163
+ };
164
+ };
165
+
166
+ // Initial message
167
+ console.log("%c⏱️ Long Tasks Tracking Active", "font-weight: bold; font-size: 14px;");
168
+ console.log(" Tasks blocking the main thread for >50ms will be logged.");
169
+ console.log(
170
+ " Call %cgetLongTaskSummary()%c for statistics.",
171
+ "font-family: monospace; background: #f3f4f6; padding: 2px 4px;",
172
+ ""
173
+ );
174
+
175
+ // Synchronous return for agent (buffered entries)
176
+ const longtaskBuffered = performance.getEntriesByType("longtask");
177
+ const totalBlockingSync = longtaskBuffered.reduce((sum, t) => sum + Math.max(0, t.duration - 50), 0);
178
+ const worstTaskSync = longtaskBuffered.length > 0 ? Math.max(...longtaskBuffered.map((t) => t.duration)) : 0;
179
+ const bySeveritySync = {
180
+ critical: longtaskBuffered.filter((t) => t.duration > 250).length,
181
+ high: longtaskBuffered.filter((t) => t.duration > 150 && t.duration <= 250).length,
182
+ medium: longtaskBuffered.filter((t) => t.duration > 100 && t.duration <= 150).length,
183
+ low: longtaskBuffered.filter((t) => t.duration >= 50 && t.duration <= 100).length,
184
+ };
185
+ return {
186
+ script: "LongTask",
187
+ status: "tracking",
188
+ count: longtaskBuffered.length,
189
+ details: {
190
+ totalBlockingTimeMs: Math.round(totalBlockingSync),
191
+ worstTaskMs: Math.round(worstTaskSync),
192
+ bySeverity: bySeveritySync,
193
+ },
194
+ message: "Tracking long tasks. Call getLongTaskSummary() for statistics.",
195
+ getDataFn: "getLongTaskSummary",
196
+ };
197
+
198
+ } catch (e) {
199
+ console.error("%c⚠️ Long Tasks API not supported", "font-weight: bold;");
200
+ console.error(" Error:", e.message);
201
+ return { script: "LongTask", status: "unsupported", error: e.message };
202
+ }
203
+ })();