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.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

  // @shared supportsRenderBlockingStatus
  // renderBlockingStatus (Resource Timing) is Chromium-only (Chrome 107+). Without it, which
  // resources block rendering is unknown: a snippet reports that, not "none".
  function supportsRenderBlockingStatus() {
    return !!window.PerformanceResourceTiming && "renderBlockingStatus" in window.PerformanceResourceTiming.prototype;
  }
  // @end-shared supportsRenderBlockingStatus

  // 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, for a page on example.com whose CDN is example.net:
  // const OWN_DOMAINS = ["example.net"];
  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

  // Without it, renderBlocking is null for every script instead of false
  const renderBlockingSupported = supportsRenderBlockingStatus();

  // A script fetched through <link rel="preload" as="script"> or <link rel="modulepreload"> has
  // initiatorType "link" or "other", not "script", so its URL is matched against the document
  const documentScriptUrls = new Set(
    Array.from(
      document.querySelectorAll('script[src], link[rel~="preload" i][as="script" i], link[rel~="modulepreload" i]'),
      (el) => (el.localName === "link" ? el.href : el.src)
    )
  );
  const isScript = (r) => r.initiatorType === "script" || documentScriptUrls.has(r.name);

  // One entry per URL: a script preloaded in one mode and loaded in another is fetched twice, and
  // the entry of the <script> is the one that tells whether it blocks rendering
  const scriptEntries = new Map();
  performance
    .getEntriesByType("resource").filter((e) => !isOwnRequest(e))
    .filter(isScript)
    .forEach((r) => {
      const seen = scriptEntries.get(r.name);
      if (!seen || (seen.initiatorType !== "script" && r.initiatorType === "script")) scriptEntries.set(r.name, r);
    });

  // Gather script data
  const scripts = [...scriptEntries.values()]
    .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: renderBlockingSupported ? r.renderBlockingStatus === "blocking" : null,
      };
    });

  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: renderBlockingSupported ? list.filter((s) => s.renderBlocking).length : null,
    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 ?? "n/a"}`);
    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 === null ? "n/a" : 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 ?? "n/a"}`);
    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 === null ? "n/a" : 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://example.com">',
        "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();
  }

  if (!renderBlockingSupported && totalScripts > 0) {
    console.log("");
    console.log("%cℹ️ renderBlockingStatus is not supported; render-blocking status is unavailable (Chrome 107+)", "color: #3b82f6;");
  }

  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,
      renderBlockingSupported,
    },
    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` }]
        : []),
      ...(!renderBlockingSupported && totalScripts > 0
        ? [{ severity: "info", message: "renderBlockingStatus is not supported in this browser (Chrome 107+), so which scripts block rendering is unknown; run the snippet in Chrome or Edge to check" }]
        : []),
      ...(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

Which requests count as scripts

A script is a Resource Timing entry with an initiatorType of script, or a request whose URL matches a <script src>, a <link rel="preload" as="script"> or a <link rel="modulepreload"> in the document. A preloaded script is fetched by the <link>, so its entry has an initiatorType of link or other, not script. Each URL counts once: when the browser fetches a script twice, for example because the preload and the <script> use different crossorigin modes, the entry of the <script> is the one kept.

The render-blocking status comes from renderBlockingStatus, which only Chromium browsers report (Chrome 107+, Edge 107+). Elsewhere details.renderBlockingSupported is false, renderBlocking and details.thirdPartyBlockingCount are null, and the snippet adds an info note instead of reporting that no script blocks rendering.

How first-party detection works

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

Page DomainScript HostDetected As
example.comexample.comFirst-party
example.comcdn.example.comFirst-party
example.comassets.example.comFirst-party
example.comanalytics.example.orgThird-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 example.net for example.com, 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 = ["example.net"];. 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.5.

Further reading

Last updated on