Long animation frames script attribution
Chrome 123+ Edge 123+ Firefox Safari Details
Analyzes and visualizes which scripts are responsible for blocking the main thread. This snippet categorizes blocking time by script origin (your code, framework, third-party, extensions) and provides actionable insights for optimization.
Why script attribution matters
- Identify if blocking comes from your code vs third-parties
- Prioritize optimization efforts on the biggest offenders
- Separate framework overhead from application code
- Track third-party impact on performance
- Segment RUM data by script category
What you'll see
- Blocking time breakdown by script category (first-party, framework, third-party, unknown)
- Top blocking scripts ranked by total duration
- Visual ASCII chart showing contribution percentages
- Specific file names and function names responsible
- Actionable recommendations based on data
Overview
Purpose: Provide detailed attribution of main thread blocking to specific scripts, categorized by origin and ranked by impact.
Key features
- Automatic Categorization: Classifies scripts as first-party, framework, third-party, or unknown
- Visual Breakdown: ASCII charts showing percentage contribution
- Top Offenders: Ranked list of scripts by total blocking time
- Actionable Insights: Specific recommendations based on findings
- RUM Integration: Export data in format ready for analytics
Use cases
- Performance audits: "Where should we optimize first?"
- Third-party analysis: "Is our analytics/ads slowing us down?"
- Framework cost: "How much overhead does React/Vue add?"
- Before/after comparisons: "Did our optimization work?"
Script attribution analysis
Snippet
// Long Animation Frames - Script Attribution Analysis
// https://webperf-snippets.nucliweb.net
(() => {
// Check browser support
if (!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame")) {
console.log("%cβ οΈ Long Animation Frames not supported", "color: #f59e0b; font-weight: bold;");
console.log("Chrome 116+ required.");
return { script: "Long-Animation-Frames-Script-Attribution", status: "unsupported", error: "long-animation-frame not supported. Chrome 116+ required." };
}
// Read buffered LoAF entries immediately
const frames = performance.getEntriesByType("long-animation-frame").map((entry) => ({
duration: entry.duration,
blockingDuration: entry.blockingDuration || 0,
scripts: (entry.scripts || []).map((s) => ({
sourceURL: s.sourceURL || "unknown",
sourceFunctionName: s.sourceFunctionName || "anonymous",
invoker: s.invoker || "unknown",
duration: s.duration || 0,
})),
}));
if (frames.length === 0) {
console.log("%cβ
No long frames detected", "color: #22c55e; font-weight: bold;");
return { script: "Long-Animation-Frames-Script-Attribution", status: "ok",
details: { frameCount: 0, totalBlockingMs: 0, byCategory: {} }, items: [] };
}
const allScripts = frames.flatMap((f) => f.scripts);
const totalBlocking = frames.reduce((sum, f) => sum + f.blockingDuration, 0);
const totalDuration = frames.reduce((sum, f) => sum + f.duration, 0);
const categorize = (url) => {
const u = url.toLowerCase();
const origin = location.origin.toLowerCase();
if (u === "" || u === "unknown") return "unknown";
if (["react", "vue", "angular", "svelte", "framework", "chunk", "webpack", "vite"].some((fw) => u.includes(fw))) return "framework";
if (["google-analytics", "gtag", "gtm", "analytics", "facebook", "twitter", "ads", "cdn", "unpkg", "jsdelivr", "segment", "amplitude"].some((tp) => u.includes(tp))) return "third-party";
if (u.startsWith("chrome-extension://") || u.startsWith("moz-extension://")) return "extension";
if (u.startsWith("http") && !u.includes(origin.replace(/^https?:\/\//, ""))) return "third-party";
return "first-party";
};
const byCategory = {};
allScripts.forEach((script) => {
const cat = categorize(script.sourceURL);
if (!byCategory[cat]) byCategory[cat] = { durationMs: 0, count: 0 };
byCategory[cat].durationMs += script.duration;
byCategory[cat].count++;
});
const byFile = {};
allScripts.forEach((script) => {
const file = script.sourceURL.split("/").pop() || "unknown";
if (!byFile[file]) byFile[file] = { duration: 0, count: 0, category: categorize(script.sourceURL), functions: [] };
byFile[file].duration += script.duration;
byFile[file].count++;
byFile[file].functions.push({ name: script.sourceFunctionName, invoker: script.invoker, duration: script.duration });
});
const topFiles = Object.entries(byFile).sort((a, b) => b[1].duration - a[1].duration).slice(0, 10);
const totalScript = Object.values(byCategory).reduce((sum, cat) => sum + cat.durationMs, 0);
const categories = [
{ name: "Your Code", key: "first-party", icon: "π΅", color: "#3b82f6" },
{ name: "Framework", key: "framework", icon: "π£", color: "#8b5cf6" },
{ name: "Third-Party", key: "third-party", icon: "π ", color: "#f59e0b" },
{ name: "Extensions", key: "extension", icon: "π€", color: "#92400e" },
{ name: "Unknown", key: "unknown", icon: "β«", color: "#6b7280" },
];
console.log("%cββββββββββββββββββββββββββββββββββββββββββββββ", "color: #6b7280;");
console.log("%cπ Blocking Time by Category", "font-weight: bold; font-size: 13px;");
console.log("");
categories.forEach(({ name, key, icon, color }) => {
const data = byCategory[key];
if (data?.durationMs > 0) {
const pct = totalScript > 0 ? (data.durationMs / totalScript) * 100 : 0;
const barLen = Math.round(pct / 2);
const bar = "β".repeat(barLen) + "β".repeat(Math.max(0, 50 - barLen));
console.log(
`%c${icon} ${name.padEnd(12)} %c${bar} %c${pct.toFixed(1)}% %c(${data.durationMs.toFixed(0)}ms, ${data.count} scripts)`,
`color: ${color}; font-weight: bold;`, `color: ${color};`, `color: ${color}; font-weight: bold;`, "color: #6b7280;",
);
}
});
console.log("");
console.log(`%cTotal Script: ${totalScript.toFixed(0)}ms | Blocking: ${totalBlocking.toFixed(0)}ms | Duration: ${totalDuration.toFixed(0)}ms`, "color: #6b7280;");
console.log(`%cFrames captured: ${frames.length}`, "color: #6b7280;");
console.log("");
console.log("%cββββββββββββββββββββββββββββββββββββββββββββββ", "color: #6b7280;");
console.log("%cπ₯ Top 10 Blocking Scripts", "font-weight: bold; font-size: 13px;");
console.log("");
topFiles.forEach(([file, data], idx) => {
const pct = totalScript > 0 ? (data.duration / totalScript) * 100 : 0;
const icon = categories.find((c) => c.key === data.category)?.icon || "β«";
console.group(
`%c${idx + 1}. ${icon} ${file} %c(${data.duration.toFixed(0)}ms, ${pct.toFixed(1)}%)`,
"font-weight: bold;", "color: #6b7280;",
);
console.log(`Category: ${data.category}`);
console.log(`Executions: ${data.count}`);
console.log(`Avg duration: ${(data.duration / data.count).toFixed(0)}ms`);
const topFns = data.functions.sort((a, b) => b.duration - a.duration).slice(0, 3);
if (topFns.length > 0) {
console.log("Top functions:");
topFns.forEach((fn, i) => console.log(` ${i + 1}. ${fn.name} (${fn.duration.toFixed(0)}ms) - ${fn.invoker}`));
}
console.groupEnd();
});
console.log("");
console.log("%cβ
Analysis complete!", "color: #22c55e; font-weight: bold;");
return {
script: "Long-Animation-Frames-Script-Attribution",
status: "ok",
details: {
frameCount: frames.length,
totalBlockingMs: Math.round(totalBlocking),
byCategory: Object.fromEntries(
Object.entries(byCategory).map(([k, v]) => [k, { durationMs: Math.round(v.durationMs), count: v.count }])
),
},
items: topFiles.map(([file, data]) => ({
file,
category: data.category,
durationMs: Math.round(data.duration),
count: data.count,
functions: [...data.functions]
.sort((a, b) => b.duration - a.duration)
.slice(0, 5)
.map((fn) => ({
name: fn.name,
invoker: fn.invoker,
durationMs: Math.round(fn.duration),
})),
})),
};
})();
Quick tips
Custom collection duration
The script runs for 10 seconds by default. You can specify a different duration:
// Quick 3-second check
analyzeScriptAttribution(3);
// Extended 20-second analysis
analyzeScriptAttribution(20);
// Or use default (10 seconds)
analyzeScriptAttribution();Already have some interactions?
Since the script uses buffered: true, it captures frames that already occurred. If you've been interacting with the page, you'll see data immediately even with shorter durations.
Re-run the analysis
The function is available in the console, so you can run it multiple times to compare different scenarios:
// After feature A
await analyzeScriptAttribution(5);
// After feature B
await analyzeScriptAttribution(5);Understanding the output
Category definitions
| Category | What it includes | Optimization Responsibility |
|---|---|---|
| Your Code | Same-origin scripts you deployed | You can fix - Your optimization target |
| Framework | React, Vue, Angular, build tool chunks | Partially controllable - Framework choice + usage patterns |
| Third-Party | Analytics, ads, CDN scripts, external APIs | Limited control - Can defer, replace, or remove |
| Extensions | Browser extensions (adblockers, etc.) | No control - User's environment |
| Unknown | Inline scripts, eval(), no source attribution | Need investigation - Likely your code |
Reading the visual chart
π΅ Your Code ββββββββββββββββββββββββββββββββ 40.2% (235ms, 3 scripts)
π£ Framework ββββββββββββββββββββββββββββββββ 55.8% (326ms, 8 scripts)
π Third-Party ββββββββββββββββββββββββββββββββ 4.0% (23ms, 2 scripts)What this tells you
- Bar length = Percentage of total script execution time
- Percentage = Contribution to blocking (prioritization metric)
- Duration = Absolute time spent (optimization potential)
- Script count = Number of individual script executions
In this example
- Framework is taking >50% β Investigate React re-renders
- Your code is 40% β Largest controllable opportunity
- Third-party is <5% β Not the problem
Interpreting recommendations
Priority levels
| Priority | When Shown | Action Required |
|---|---|---|
| High | >30% from your code | Immediate action - Largest impact opportunity |
| Medium | >40% from framework, >20% from third-party | Plan optimization - Significant but requires careful approach |
| Low | Minor contributors | Monitor - Optimize if easy wins available |
| Info | Extensions or environmental | Document - Track in RUM, no code changes needed |
RUM data structure
The exported rumData object structure:
{
timestamp: "2024-01-15T10:30:00.000Z",
summary: {
frameCount: 11,
totalDuration: 1200, // Total frame time
totalBlockingTime: 450, // Time blocking input
totalScriptTime: 584, // Total JS execution
},
byCategory: {
"first-party": { duration: 235, percentage: 40.2, count: 3 },
"framework": { duration: 326, percentage: 55.8, count: 8 },
"third-party": { duration: 23, percentage: 4.0, count: 2 },
// ...
},
topScripts: [
{
file: "4617.de2e87c94f999416.js",
category: "first-party",
duration: 235,
percentage: 40.2,
executions: 2
},
// ...
]
}How to use in RUM
- Segment by category % - Track if third-party cost increases over time
- Alert on spikes - If first-party > 50%, investigate recent deploys
- Compare P50/P95 - See if some users have worse attribution
- A/B test impact - Did removing analytics improve frameRate?
Browser support
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| PerformanceLongAnimationFrameTiming (opens in a new tab) | 123 | 123 | ||
| PerformanceScriptTiming (opens in a new tab) | 123 | 123 | ||
| PerformanceObserver (opens in a new tab) | 52 | 79 | 57 | 11 |
| All of the above | 123 | 123 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.
Fallback: For browsers without LoAF, use the Long Tasks API but you'll lose script attribution detail.
Best practices
When to run this analysis
Good times:
- During performance audits
- After major feature launches
- When investigating user complaints about sluggishness
- Before/after optimization to measure impact
- During development to catch regressions early
Avoid:
- In production on every page load (use sampling)
- On low-end devices (adds overhead)
- Without user interaction (won't capture interaction frames)
How to get good data
- Interact with the page - Click, scroll, type during the collection window (default: 10 seconds)
- Test realistic scenarios - Open dropdowns, submit forms, load data
- Adjust duration as needed - Use
analyzeScriptAttribution(3)for quick checks,analyzeScriptAttribution(20)for comprehensive analysis - Run multiple times - Compare first load vs. cached, different user flows
- Compare environments - Development vs. production builds
- Test on real devices - Mobile performance often differs significantly
- Use buffered data - The script captures frames that already occurred, so existing interactions count
Integration tips
// Only run on a sample of users (10%) with short duration
if (Math.random() < 0.1) {
analyzeScriptAttribution(5).then((data) => {
sendToRUM(data);
});
}
// Run after page is interactive
if (document.readyState === "complete") {
analyzeScriptAttribution();
} else {
window.addEventListener("load", () => {
setTimeout(() => analyzeScriptAttribution(), 2000); // Wait 2s after load
});
}
// Run on specific user interactions (quick check)
document.querySelector("#analyze-btn")?.addEventListener("click", () => {
analyzeScriptAttribution(3).then((data) => {
console.log("Quick analysis complete:", data);
});
});
// Programmatic analysis with custom duration
async function checkPerformance() {
const data = await analyzeScriptAttribution(5);
if (data.byCategory["third-party"].duration > 1000) {
console.warn("High third-party blocking time detected!");
}
return data;
}Real-world examples
Example 1: Framework-heavy application
π Blocking Time by Category
π΅ Your Code ββββββββββββββββββββββββββββββββ 12.3% (89ms)
π£ Framework ββββββββββββββββββββββββββββββββ 78.9% (571ms)
π Third-Party ββββββββββββββββββββββββββββββββ 8.8% (64ms)Diagnosis: Framework overhead is dominating (79%) Action: Implement React.memo(), reduce re-renders, consider Preact
Example 2: Third-party impact
π Blocking Time by Category
π΅ Your Code ββββββββββββββββββββββββββββββββ 24.5% (145ms)
π£ Framework ββββββββββββββββββββββββββββββββ 20.1% (119ms)
π Third-Party ββββββββββββββββββββββββββββββββ 55.4% (328ms)Diagnosis: Third-parties taking >50% (analytics, ads, tracking) Action: Defer non-critical scripts, use facades, consider alternatives
Example 3: Balanced (optimized)
π Blocking Time by Category
π΅ Your Code ββββββββββββββββββββββββββββββββ 25.0% (78ms)
π£ Framework ββββββββββββββββββββββββββββββββ 32.1% (100ms)
π Third-Party ββββββββββββββββββββββββββββββββ 24.4% (76ms)
β« Unknown ββββββββββββββββββββββββββββββββ 18.5% (58ms)Diagnosis: Well-distributed, no single category dominates Action: Focus on absolute values - can all categories go lower?