Skip to Content
LoadingFirst And Third Party Script Info

First and third party script info

Chrome 54+ Edge 17+ Firefox 45+ Safari 16.4+ Details

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

ConcernFirst-PartyThird-Party
ControlFull control over optimizationLimited or no control
ReliabilityDepends on your infrastructureExternal point of failure
PerformanceCan be optimized, bundled, cachedOften unoptimized, may block rendering
PrivacyYour data policiesMay collect user data
SecurityYour responsibilityPotential 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)

Related: Use the Find render-blocking resources snippet to identify scripts blocking initial render.

Snippet

Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.

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

(() => {
  // 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((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

FieldDescription
ScriptFilename
HostDomain serving the script
SizeTransfer size (compressed); unknown for cross-origin scripts without Timing-Allow-Origin
DurationTotal time to load
BlockingWhether it blocks rendering

How first-party detection works

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

Page DomainScript HostDetected As
web.devweb.devFirst-party
web.devcdn.web.devFirst-party
web.devassets.web.devFirst-party
web.devgoogle-analytics.comThird-party
shop.example.co.ukcdn.example.co.ukFirst-party
www.example.ac.ukstatic.example.ac.ukFirst-party
10.0.0.1172.16.0.1Third-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.

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

CategoryExamplesTypical Impact
AnalyticsGoogle Analytics, Segment, Mixpanel20-50 KB, often async
AdvertisingGoogle Ads, Facebook Pixel50-200 KB, may chain-load
Tag ManagersGTM, Tealium30-80 KB, loads additional scripts
SocialFacebook SDK, Twitter widgets100-300 KB, heavy
Chat/SupportIntercom, Zendesk, Drift100-500 KB, often deferred
A/B TestingOptimizely, VWO50-150 KB, often blocking

Browser support

The snippet needs these features to run.

FeatureChromeEdgeFirefoxSafari
PerformanceResourceTiming.transferSize (opens in a new tab) 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.

FeatureChromeEdgeFirefoxSafari
PerformanceResourceTiming.renderBlockingStatus (opens in a new tab) 107 107

Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.

Further reading

Last updated on
MIT 2026 © Joan León | @nucliweb