# First and third party script info

> Source: <https://webperf-snippets.net/Loading/First-And-Third-Party-Script-Info>

Analyzes all scripts loaded on the page, separating them into first-party (your domain) and third-party (external) scripts. This helps identify the performance impact of external dependencies.

### Why this matters

| Concern         | First-Party                       | Third-Party                            |
| --------------- | --------------------------------- | -------------------------------------- |
| **Control**     | Full control over optimization    | Limited or no control                  |
| **Reliability** | Depends on your infrastructure    | External point of failure              |
| **Performance** | Can be optimized, bundled, cached | Often unoptimized, may block rendering |
| **Privacy**     | Your data policies                | May collect user data                  |
| **Security**    | Your responsibility               | Potential vulnerability vector         |

### Common third-party script issues

- Block main thread during execution
- Add DNS lookups and connection overhead
- May load additional scripts (script chains)
- Often not cached effectively
- Can delay Time to Interactive (TTI)

> **Note:** **Related:** Use the [Find render-blocking resources](https://webperf-snippets.net/Loading/Find-render-blocking-resources.md) snippet to identify scripts blocking initial render.

### Snippet

```js
// First and Third Party Script Analysis
// https://webperf-snippets.net

(() => {
  // @shared isOwnRequest
  // True for the Resource Timing entry of a request a snippet made with ownRequestUrl: the page
  // did not make it, so no snippet counts it.
  function isOwnRequest(entry) {
    return entry.name.endsWith("#webperf-snippets");
  }
  // @end-shared isOwnRequest

  // Domains that belong to the site but differ from the page's root domain, such as its own CDN.
  // They count as first party. Example: const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];
  const OWN_DOMAINS = [];

  // Auto-detect first-party: same root domain (handles subdomains)
  // @shared getRootDomain
  function getRootDomain(hostname) {
    const host = hostname.replace(/\.$/, "");
    // An IP address has no registrable domain, so each address is its own root
    if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":")) return host;
    const parts = host.split(".");
    if (parts.length <= 2) return host;
    // Country-code domains with a second-level suffix: example.co.uk, example.com.au, example.ac.jp
    const secondLevelSuffixes = ["ac", "co", "com", "edu", "go", "gob", "gouv", "gov", "govt", "mil", "ne", "net", "nom", "or", "org", "sch"];
    const tld = parts[parts.length - 1];
    const sld = parts[parts.length - 2];
    if (tld.length === 2 && secondLevelSuffixes.includes(sld)) return parts.slice(-3).join(".");
    return parts.slice(-2).join(".");
  }
  // @end-shared getRootDomain

  // @shared isFirstParty
  function isFirstParty(hostname) {
    const root = getRootDomain(hostname);
    if (root === getRootDomain(location.hostname)) return true;
    return OWN_DOMAINS.some((d) => getRootDomain(String(d).trim().toLowerCase().replace(/^[a-z]+:\/\//, "").split("/")[0]) === root);
  }
  // @end-shared isFirstParty

  // @shared logOwnDomainsHint
  function logOwnDomainsHint(thirdPartyCount) {
    if (OWN_DOMAINS.length === 0 && thirdPartyCount > 0) {
      console.log(
        '%cℹ️ OWN_DOMAINS is empty. If this site serves its own assets from other domains (for example a CDN), add them at the top of the snippet, such as const OWN_DOMAINS = ["cdn.example.net"];, and run it again so they count as first party.',
        "color: #3b82f6;"
      );
    }
  }
  // @end-shared logOwnDomainsHint

  // @shared formatBytes
  function formatBytes(bytes) {
    if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
    if (bytes === 0) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
    return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
  }
  // @end-shared formatBytes

  // Gather script data
  const scripts = performance
    .getEntriesByType("resource").filter((e) => !isOwnRequest(e))
    .filter((r) => r.initiatorType === "script")
    .map((r) => {
      const url = new URL(r.name);
      const firstParty = isFirstParty(url.hostname);

      return {
        name: r.name,
        shortName: url.pathname.split("/").pop() || url.pathname,
        host: url.hostname,
        firstParty,
        duration: r.duration,
        transferSize: r.transferSize || 0,
        // Cross-origin scripts without Timing-Allow-Origin report every size as 0
        sizeKnown: r.transferSize > 0 || r.encodedBodySize > 0 || r.decodedBodySize > 0,
        startTime: r.startTime,
        responseEnd: r.responseEnd,
        renderBlocking: r.renderBlockingStatus === "blocking",
      };
    });

  const firstParty = scripts.filter((s) => s.firstParty);
  const thirdParty = scripts.filter((s) => !s.firstParty);

  // Calculate metrics
  const calcMetrics = (list) => ({
    count: list.length,
    totalSize: list.reduce((sum, s) => sum + s.transferSize, 0),
    totalDuration: list.reduce((sum, s) => sum + s.duration, 0),
    blocking: list.filter((s) => s.renderBlocking).length,
    hosts: [...new Set(list.map((s) => s.host))],
  });

  const firstMetrics = calcMetrics(firstParty);
  const thirdMetrics = calcMetrics(thirdParty);
  const totalScripts = scripts.length;
  const thirdPartyPct =
    totalScripts > 0 ? ((thirdParty.length / totalScripts) * 100).toFixed(0) : 0;

  // Display results
  console.group(
    "%c📊 Script Analysis: First vs Third Party",
    "font-weight: bold; font-size: 14px;",
  );

  // Overall summary
  console.log("");
  console.log("%cOverall Summary:", "font-weight: bold;");
  console.log(`   Total scripts: ${totalScripts}`);
  console.log(`   First-party: ${firstParty.length} (${100 - thirdPartyPct}%)`);
  console.log(`   Third-party: ${thirdParty.length} (${thirdPartyPct}%)`);

  if (thirdParty.length > firstParty.length) {
    console.log(`%c   ⚠️ More third-party scripts than first-party!`, "color: #f59e0b;");
  }

  // First Party Section
  console.log("");
  console.group(
    `%c🏠 First-Party Scripts (${firstParty.length})`,
    "color: #22c55e; font-weight: bold;",
  );

  if (firstParty.length === 0) {
    console.log("No first-party scripts found.");
  } else {
    console.log(`   Total size: ${formatBytes(firstMetrics.totalSize)}`);
    console.log(`   Render-blocking: ${firstMetrics.blocking}`);
    console.log("");

    const firstTable = firstParty
      .sort((a, b) => b.transferSize - a.transferSize)
      .map((s) => ({
        Script: s.shortName,
        Size: formatBytes(s.transferSize),
        Duration: s.duration.toFixed(0) + "ms",
        Blocking: s.renderBlocking ? "⚠️ Yes" : "No",
        Host: s.host,
      }));
    console.table(firstTable);
  }
  console.groupEnd();

  // Third Party Section
  console.log("");
  console.group(
    `%c🌐 Third-Party Scripts (${thirdParty.length})`,
    "color: #ef4444; font-weight: bold;",
  );

  if (thirdParty.length === 0) {
    console.log("%c✅ No third-party scripts found!", "color: #22c55e; font-weight: bold;");
  } else {
    console.log(`   Total size: ${formatBytes(thirdMetrics.totalSize)}`);
    console.log(`   Render-blocking: ${thirdMetrics.blocking}`);
    console.log(`   Unique hosts: ${thirdMetrics.hosts.length}`);
    console.log("");

    // Group by host
    console.log("%c   By host:", "font-weight: bold;");
    const byHost = thirdParty.reduce((acc, s) => {
      if (!acc[s.host]) acc[s.host] = { count: 0, size: 0, blocking: 0 };
      acc[s.host].count++;
      acc[s.host].size += s.transferSize;
      if (s.renderBlocking) acc[s.host].blocking++;
      return acc;
    }, {});

    Object.entries(byHost)
      .sort((a, b) => b[1].size - a[1].size)
      .forEach(([host, data]) => {
        const blockingMark = data.blocking > 0 ? " ⚠️" : "";
        console.log(
          `      ${host}: ${data.count} script(s), ${formatBytes(data.size)}${blockingMark}`,
        );
      });

    console.log("");
    const thirdTable = thirdParty
      .sort((a, b) => b.transferSize - a.transferSize)
      .map((s) => ({
        Script: s.shortName,
        Host: s.host,
        Size: formatBytes(s.transferSize),
        Duration: s.duration.toFixed(0) + "ms",
        Blocking: s.renderBlocking ? "⚠️ Yes" : "No",
      }));
    console.table(thirdTable);
  }
  console.groupEnd();

  // Recommendations
  if (thirdParty.length > 0) {
    console.log("");
    console.group("%c📝 Recommendations", "color: #3b82f6; font-weight: bold;");

    if (thirdMetrics.blocking > 0) {
      console.log("");
      console.log(
        "%c⚠️ Render-blocking third-party scripts:",
        "font-weight: bold; color: #ef4444;",
      );
      console.log("   • Load with 'async' or 'defer' attribute");
      console.log("   • Consider lazy-loading after user interaction");
      console.log(
        '%c   <script src="..." async></script>',
        "font-family: monospace; color: #22c55e;",
      );
    }

    if (thirdMetrics.hosts.length > 3) {
      console.log("");
      console.log(
        `%c⚠️ ${thirdMetrics.hosts.length} different third-party hosts:`,
        "font-weight: bold; color: #f59e0b;",
      );
      console.log("   • Each host requires DNS lookup + connection");
      console.log("   • Use <link rel='preconnect'> for critical hosts");
      console.log(
        '%c   <link rel="preconnect" href="https://web.dev">',
        "font-family: monospace; color: #22c55e;",
      );
    }

    if (thirdMetrics.totalSize > 100 * 1024) {
      console.log("");
      console.log(
        `%c⚠️ Third-party scripts total ${formatBytes(thirdMetrics.totalSize)}:`,
        "font-weight: bold; color: #f59e0b;",
      );
      console.log("   • Audit necessity of each script");
      console.log("   • Consider self-hosting critical scripts");
      console.log("   • Look for lighter alternatives");
    }

    console.log("");
    console.log("%c💡 General tips:", "font-weight: bold;");
    console.log("   • Regularly audit third-party scripts");
    console.log("   • Set up Content Security Policy (CSP)");
    console.log("   • Monitor third-party performance with RUM");
    console.log("   • Have fallbacks for critical functionality");

    console.groupEnd();
  }

  logOwnDomainsHint(thirdParty.length);
  console.groupEnd();

  // The list keeps the 50 scripts to look at first (render-blocking, then third-party, then the largest); count and details cover all of them
  const MAX_ITEMS = 50;
  const rankedScripts = [...scripts].sort(
    (a, b) =>
      Number(b.renderBlocking) - Number(a.renderBlocking) ||
      Number(a.firstParty) - Number(b.firstParty) ||
      b.transferSize - a.transferSize
  );
  return {
    script: "First-And-Third-Party-Script-Info",
    status: "ok",
    count: totalScripts,
    corsLimitedAnalysis: scripts.some((s) => !s.sizeKnown),
    details: {
      firstPartyCount: firstParty.length,
      thirdPartyCount: thirdParty.length,
      thirdPartyPercent: Number(thirdPartyPct),
      firstPartySizeBytes: firstMetrics.totalSize,
      thirdPartySizeBytes: thirdMetrics.totalSize,
      thirdPartyBlockingCount: thirdMetrics.blocking,
      thirdPartyHostCount: thirdMetrics.hosts.length,
      sizeUnknownCount: scripts.filter((s) => !s.sizeKnown).length,
    },
    items: rankedScripts.slice(0, MAX_ITEMS).map((s) => ({
      shortName: s.shortName,
      host: s.host,
      firstParty: s.firstParty,
      transferBytes: s.transferSize,
      sizeKnown: s.sizeKnown,
      durationMs: Math.round(s.duration),
      renderBlocking: s.renderBlocking,
    })),
    issues: [
      ...(scripts.some((s) => !s.sizeKnown)
        ? [{ severity: "info", message: `${scripts.filter((s) => !s.sizeKnown).length} script(s) have an unknown size (missing Timing-Allow-Origin); size totals are a lower bound` }]
        : []),
      ...(thirdMetrics.blocking > 0
        ? [
            {
              severity: "error",
              message: `${thirdMetrics.blocking} render-blocking third-party script(s)`,
            },
          ]
        : []),
      ...(thirdMetrics.hosts.length > 3
        ? [
            {
              severity: "warning",
              message: `${thirdMetrics.hosts.length} different third-party hosts require separate DNS lookups`,
            },
          ]
        : []),
      ...(thirdMetrics.totalSize > 100 * 1024
        ? [
            {
              severity: "warning",
              message: `Third-party scripts total ${Math.round(thirdMetrics.totalSize / 1024)} KB`,
            },
          ]
        : []),
    ],
  };
})();
```

### Understanding the results

### Overall summary

- Total script count with first/third-party breakdown
- Warning if third-party scripts outnumber first-party

### First-party scripts

- Scripts from your domain and subdomains (auto-detected)
- Total size and render-blocking count

### Third-party scripts

- Scripts from external domains
- Grouped by host to identify heavy dependencies
- Total size, render-blocking count, unique hosts

### For each script

| Field    | Description                                                                                |
| -------- | ------------------------------------------------------------------------------------------ |
| Script   | Filename                                                                                   |
| Host     | Domain serving the script                                                                  |
| Size     | Transfer size (compressed); unknown for cross-origin scripts without `Timing-Allow-Origin` |
| Duration | Total time to load                                                                         |
| Blocking | Whether it blocks rendering                                                                |

### How first-party detection works

The snippet automatically detects first-party scripts by comparing root domains:

| Page Domain          | Script Host            | Detected As |
| -------------------- | ---------------------- | ----------- |
| `web.dev`            | `web.dev`              | First-party |
| `web.dev`            | `cdn.web.dev`          | First-party |
| `web.dev`            | `assets.web.dev`       | First-party |
| `web.dev`            | `google-analytics.com` | Third-party |
| `shop.example.co.uk` | `cdn.example.co.uk`    | First-party |
| `www.example.ac.uk`  | `static.example.ac.uk` | First-party |
| `10.0.0.1`           | `172.16.0.1`           | Third-party |

An IP address is compared as a whole, so two different addresses are never the same party. Script-Loading, Cache-Strategy-Analysis, TTFB-Resources, First-And-Third-Party-Script-Timings and the font preload check apply the same rule.

> **Note:** Scripts are first party when they share the page's root domain. A site's own CDN on a different root domain, such as `bbci.co.uk` for `bbc.co.uk`, is reported as a third party, because the comparison uses root domains only. To count those domains as first party, list them in the `OWN_DOMAINS` constant at the top of the snippet, for example `const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];`. Entries can be domains or URLs. While the list is empty and the page has third-party resources, the snippet prints a reminder in the console.

### Common third-party script categories

| Category     | Examples                            | Typical Impact                     |
| ------------ | ----------------------------------- | ---------------------------------- |
| Analytics    | Google Analytics, Segment, Mixpanel | 20-50 KB, often async              |
| Advertising  | Google Ads, Facebook Pixel          | 50-200 KB, may chain-load          |
| Tag Managers | GTM, Tealium                        | 30-80 KB, loads additional scripts |
| Social       | Facebook SDK, Twitter widgets       | 100-300 KB, heavy                  |
| Chat/Support | Intercom, Zendesk, Drift            | 100-500 KB, often deferred         |
| A/B Testing  | Optimizely, VWO                     | 50-150 KB, often blocking          |

### Browser support

The snippet needs these features to run.

| Feature                                                                                                                     | Chrome | Edge | Firefox | Safari |
| --------------------------------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [PerformanceResourceTiming.transferSize](https://developer.mozilla.org/docs/Web/API/PerformanceResourceTiming/transferSize) |  ✅ 54  | ✅ 17 |   ✅ 45  | ✅ 16.4 |

The snippet reports on these features. It runs without them, and its advice on each one applies where it is supported.

| Feature                                                                                                                                     | Chrome |  Edge | Firefox | Safari |
| ------------------------------------------------------------------------------------------------------------------------------------------- | :----: | :---: | :-----: | :----: |
| [PerformanceResourceTiming.renderBlockingStatus](https://developer.mozilla.org/docs/Web/API/PerformanceResourceTiming/renderBlockingStatus) |  ✅ 107 | ✅ 107 |    ❌    |    ❌   |

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

### Further reading

- [Loading Third-Party JavaScript](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript) | web.dev
- [Best practices for third-party scripts](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript) | web.dev
- [PerformanceResourceTiming API](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming) | MDN
- [Using the Resource Timing API](https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API#coping_with_cors) | MDN
