# Long animation frames script attribution

> Source: <https://webperf-snippets.net/Interaction/Long-Animation-Frames-Script-Attribution>

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

Diagram: Report pipeline: capture long animation frames, extract scripts, categorize them as your code, framework, third-party or unknown, calculate percentages, then produce a visual report and optimization priorities

## Overview

**Purpose**: Provide detailed attribution of main thread blocking to specific scripts, categorized by origin and ranked by impact.

### Key features

1. **Automatic Categorization**: Classifies scripts as first-party, framework, third-party, or unknown
2. **Visual Breakdown**: ASCII charts showing percentage contribution
3. **Top Offenders**: Ranked list of scripts by total blocking time
4. **Actionable Insights**: Specific recommendations based on findings
5. **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

```js
// Long Animation Frames - Script Attribution Analysis
// https://webperf-snippets.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:

```javascript copy
// 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:

```javascript copy
// 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:

```javascript
{
  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](https://developer.mozilla.org/docs/Web/API/PerformanceLongAnimationFrameTiming) |  ✅ 123 | ✅ 123 |    ❌    |    ❌   |
| [PerformanceScriptTiming](https://developer.mozilla.org/docs/Web/API/PerformanceScriptTiming)                         |  ✅ 123 | ✅ 123 |    ❌    |    ❌   |
| [PerformanceObserver](https://developer.mozilla.org/docs/Web/API/PerformanceObserver)                                 |  ✅ 52  |  ✅ 79 |   ✅ 57  |  ✅ 11  |
| All of the above                                                                                                      |  ✅ 123 | ✅ 123 |    ❌    |    ❌   |

Source: [MDN browser compatibility data](https://github.com/mdn/browser-compat-data), version 8.1.4.

**Fallback**: For browsers without LoAF, use the [Long Tasks API](https://webperf-snippets.net/Interaction/LongTask.md) 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

1. **Interact with the page** - Click, scroll, type during the collection window (default: 10 seconds)
2. **Test realistic scenarios** - Open dropdowns, submit forms, load data
3. **Adjust duration as needed** - Use `analyzeScriptAttribution(3)` for quick checks, `analyzeScriptAttribution(20)` for comprehensive analysis
4. **Run multiple times** - Compare first load vs. cached, different user flows
5. **Compare environments** - Development vs. production builds
6. **Test on real devices** - Mobile performance often differs significantly
7. **Use buffered data** - The script captures frames that already occurred, so existing interactions count

### Integration tips

```javascript
// 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?

## Further reading

- [Long Animation Frames API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceLongAnimationFrameTiming) | MDN
- [Optimize long tasks](https://web.dev/articles/optimize-long-tasks) | web.dev
- [Script evaluation and long tasks](https://web.dev/articles/script-evaluation-and-long-tasks) | web.dev
- [Long Animation Frames Helpers](https://webperf-snippets.net/Interaction/Long-Animation-Frames-Helpers.md) | This site
- [Third-party JavaScript](https://web.dev/articles/third-party-javascript) | web.dev
