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,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
|
+
})();
|