Skip to Content
LoadingFirst And Third Party Script Timings

First and third party script timings

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

Analyzes detailed timing phases for all scripts, comparing first-party vs third-party performance. This helps identify slow connection phases, DNS issues, or server response problems.

Timing phases explained

Order of the network phases that precede a script response: redirect, DNS lookup, TCP connection, TLS handshake, request, then response start and endSteps: Browser to Browser: Redirect (if any); Browser to DNS: DNS Lookup; DNS to Browser: IP Address; Browser to Server: TCP Connection; Browser to Server: TLS/SSL Handshake; Browser to Server: Request; Server to Browser: Response Start; Server to Browser: Response End.Redirect (if any)DNS LookupIP AddressTCP ConnectionTLS/SSL HandshakeRequestResponse StartResponse EndBrowserDNSServer
PhaseWhat it measuresWhy it matters
DNS LookupTime to resolve domain nameSlow DNS affects all requests to that domain
TCP ConnectionTime to establish connectionGeographic distance, server load
TLS/SSLTime for secure handshakeCertificate chain, protocol version
RequestTime from request sent to first byteServer processing time (TTFB)
ResponseTime to download the resourceFile size, connection speed
TotalEnd-to-end loading timeOverall script impact

CORS Note: Third-party scripts without Timing-Allow-Origin header will show 0 for most timing phases. Only startTime, responseEnd, and duration are available.

Related: Use First And Third Party Script Info for a simpler overview focused on sizes and blocking status.

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 Timings 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 by root domain
  // @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

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

      // Check if timing data is available (CORS)
      const hasTiming = r.requestStart > 0;

      // Calculate timing phases
      const timings = {
        dns: r.domainLookupEnd - r.domainLookupStart,
        tcp: r.connectEnd - r.connectStart,
        tls: r.secureConnectionStart > 0 ? r.connectEnd - r.secureConnectionStart : 0,
        request: r.responseStart - r.requestStart,
        response: r.responseEnd - r.responseStart,
        total: r.responseEnd - r.startTime,
      };

      // Adjust TCP to exclude TLS time
      if (r.secureConnectionStart > 0) {
        timings.tcp = r.secureConnectionStart - r.connectStart;
      }

      // Without Timing-Allow-Origin the browser reports the phases as 0, so response would be
      // the whole duration since the epoch. Only the total is real, the phases are unknown.
      if (!hasTiming) {
        for (const phase of ["dns", "tcp", "tls", "request", "response"]) timings[phase] = null;
      }

      return {
        name: r.name,
        shortName: url.pathname.split("/").pop() || url.hostname,
        host: url.hostname,
        firstParty,
        hasTiming,
        size: r.transferSize || 0,
        ...timings,
      };
    });

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

  // Calculate aggregate metrics
  function calcStats(list) {
    const withTiming = list.filter((s) => s.hasTiming);
    const phases = ["dns", "tcp", "tls", "request", "response", "total"];

    const stats = {};
    phases.forEach((phase) => {
      const values = withTiming.map((s) => s[phase]).filter((v) => v > 0);
      if (values.length > 0) {
        stats[phase] = {
          avg: values.reduce((a, b) => a + b, 0) / values.length,
          max: Math.max(...values),
          total: values.reduce((a, b) => a + b, 0),
        };
      } else {
        stats[phase] = { avg: 0, max: 0, total: 0 };
      }
    });

    return {
      count: list.length,
      withTiming: withTiming.length,
      withoutTiming: list.length - withTiming.length,
      stats,
    };
  }

  const firstStats = calcStats(firstParty);
  const thirdStats = calcStats(thirdParty);

  // Format helpers
  const roundOrNull = (ms) => (ms === null ? null : Math.round(ms));
  const formatMs = (ms) => (ms > 0 ? ms.toFixed(1) + "ms" : "-");
  const formatBar = (value, max) => {
    if (value <= 0 || max <= 0) return "";
    const width = Math.min(Math.round((value / max) * 15), 15);
    return "█".repeat(width) + "░".repeat(15 - width);
  };

  // Display results
  console.group("%c⏱️ Script Timing Analysis", "font-weight: bold; font-size: 14px;");

  // Summary comparison
  console.log("");
  console.log("%cSummary (averages):", "font-weight: bold;");

  const phases = [
    { key: "dns", name: "DNS Lookup", icon: "🔍" },
    { key: "tcp", name: "TCP Connection", icon: "🔌" },
    { key: "tls", name: "TLS/SSL", icon: "🔒" },
    { key: "request", name: "Request (TTFB)", icon: "📤" },
    { key: "response", name: "Response", icon: "📥" },
    { key: "total", name: "Total", icon: "⏱️" },
  ];

  console.log("");
  console.log("                        First-Party    Third-Party");
  console.log("                        ───────────    ───────────");

  phases.forEach(({ key, name, icon }) => {
    const fp = formatMs(firstStats.stats[key]?.avg || 0).padStart(10);
    const tp = formatMs(thirdStats.stats[key]?.avg || 0).padStart(10);
    console.log(`${icon} ${name.padEnd(18)} ${fp}       ${tp}`);
  });

  // First-party details
  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 {
    if (firstStats.withoutTiming > 0) {
      console.log(`%c⚠️ ${firstStats.withoutTiming} script(s) have no detailed timing (cached or same-origin)`, "color: #f59e0b;");
    }

    const maxTotal = Math.max(...firstParty.map((s) => s.total));

    const tableData = firstParty
      .sort((a, b) => b.total - a.total)
      .slice(0, 15)
      .map((s) => ({
        Script: s.shortName,
        DNS: formatMs(s.dns),
        TCP: formatMs(s.tcp),
        TLS: formatMs(s.tls),
        Request: formatMs(s.request),
        Response: formatMs(s.response),
        Total: formatMs(s.total),
        "": formatBar(s.total, maxTotal),
      }));

    console.table(tableData);

    if (firstParty.length > 15) {
      console.log(`... and ${firstParty.length - 15} more scripts`);
    }
  }
  console.groupEnd();

  // Third-party details
  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!", "color: #22c55e; font-weight: bold;");
  } else {
    const corsRestricted = thirdStats.withoutTiming;
    if (corsRestricted > 0) {
      console.log(`%c⚠️ ${corsRestricted} script(s) restricted by CORS (no Timing-Allow-Origin header)`, "color: #f59e0b;");
      console.log("   Only total duration available for these scripts.");
      console.log("");
    }

    const maxTotal = Math.max(...thirdParty.map((s) => s.total));

    const tableData = thirdParty
      .sort((a, b) => b.total - a.total)
      .slice(0, 15)
      .map((s) => ({
        Script: s.shortName,
        Host: s.host.length > 25 ? s.host.slice(0, 22) + "..." : s.host,
        DNS: formatMs(s.dns),
        TCP: formatMs(s.tcp),
        TLS: formatMs(s.tls),
        Request: formatMs(s.request),
        Response: formatMs(s.response),
        Total: formatMs(s.total),
        "": formatBar(s.total, maxTotal),
      }));

    console.table(tableData);

    if (thirdParty.length > 15) {
      console.log(`... and ${thirdParty.length - 15} more scripts`);
    }
  }
  console.groupEnd();

  // Identify slow scripts
  const slowThreshold = 500; // ms
  const slowScripts = scripts.filter((s) => s.total > slowThreshold);

  if (slowScripts.length > 0) {
    console.log("");
    console.group(`%c🐌 Slow Scripts (>${slowThreshold}ms)`, "color: #ef4444; font-weight: bold;");

    slowScripts
      .sort((a, b) => b.total - a.total)
      .forEach((s) => {
        const party = s.firstParty ? "1st" : "3rd";
        const phases = [];
        if (s.dns > 50) phases.push(`DNS: ${formatMs(s.dns)}`);
        if (s.tcp > 50) phases.push(`TCP: ${formatMs(s.tcp)}`);
        if (s.tls > 50) phases.push(`TLS: ${formatMs(s.tls)}`);
        if (s.request > 100) phases.push(`Request: ${formatMs(s.request)}`);
        if (s.response > 200) phases.push(`Response: ${formatMs(s.response)}`);

        console.log(`${party} ${formatMs(s.total).padStart(8)} - ${s.shortName}`);
        if (phases.length > 0 && s.hasTiming) {
          console.log(`%c         Slow phases: ${phases.join(", ")}`, "color: #666;");
        }
      });

    console.groupEnd();
  }

  // Recommendations
  const hasSlowDns = scripts.some((s) => s.dns > 100);
  const hasSlowTcp = scripts.some((s) => s.tcp > 100);
  const hasSlowRequest = scripts.some((s) => s.request > 200);
  const hasCorsIssues = thirdStats.withoutTiming > 0;

  if (hasSlowDns || hasSlowTcp || hasSlowRequest || hasCorsIssues) {
    console.log("");
    console.group("%c📝 Recommendations", "color: #3b82f6; font-weight: bold;");

    if (hasSlowDns) {
      console.log("");
      console.log("%c🔍 Slow DNS lookups detected:", "font-weight: bold;");
      console.log("   • Use <link rel='dns-prefetch'> for third-party domains");
      console.log("   • Consider using a faster DNS provider");
    }

    if (hasSlowTcp) {
      console.log("");
      console.log("%c🔌 Slow TCP connections detected:", "font-weight: bold;");
      console.log("   • Use <link rel='preconnect'> for critical third-party origins");
      console.log("   • Enable HTTP/2 or HTTP/3 for multiplexing");
    }

    if (hasSlowRequest) {
      console.log("");
      console.log("%c📤 Slow server response times detected:", "font-weight: bold;");
      console.log("   • Optimize server processing");
      console.log("   • Use a CDN closer to users");
      console.log("   • Consider self-hosting critical third-party scripts");
    }

    if (hasCorsIssues) {
      console.log("");
      console.log("%c🔒 CORS-restricted timing data:", "font-weight: bold;");
      console.log("   • Third-party servers need Timing-Allow-Origin header");
      console.log("   • Contact vendors to enable resource timing");
      console.log("   • Self-host scripts to get full timing data");
    }

    console.groupEnd();
  }

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

  // The list keeps the 50 slowest scripts; count and details cover all of them
  const MAX_ITEMS = 50;
  const slowestScripts = [...scripts].sort((a, b) => b.total - a.total);
  return {
    script: "First-And-Third-Party-Script-Timings",
    status: "ok",
    count: scripts.length,
    corsLimitedAnalysis: thirdStats.withoutTiming > 0,
    details: {
      firstPartyCount: firstParty.length,
      thirdPartyCount: thirdParty.length,
      corsRestrictedCount: thirdStats.withoutTiming,
      slowScriptCount: slowScripts.length,
      firstPartyAvgTotalMs: Math.round(firstStats.stats.total?.avg || 0),
      thirdPartyAvgTotalMs: Math.round(thirdStats.stats.total?.avg || 0),
    },
    items: slowestScripts.slice(0, MAX_ITEMS).map(s => ({ shortName: s.shortName, host: s.host, firstParty: s.firstParty, totalMs: Math.round(s.total), dnsMs: roundOrNull(s.dns), tcpMs: roundOrNull(s.tcp), requestMs: roundOrNull(s.request), responseMs: roundOrNull(s.response), hasTiming: s.hasTiming })),
    issues: [
      ...(slowScripts.length > 0 ? [{ severity: "warning", message: `${slowScripts.length} script(s) take over ${slowThreshold}ms to load` }] : []),
      ...(hasSlowDns ? [{ severity: "warning", message: "Slow DNS lookups detected (>100ms). Add dns-prefetch or preconnect." }] : []),
      ...(hasSlowTcp ? [{ severity: "warning", message: "Slow TCP connections detected (>100ms). Add preconnect for critical origins." }] : []),
      ...(hasSlowRequest ? [{ severity: "warning", message: "Slow server response times detected (>200ms). Consider CDN or self-hosting." }] : []),
    ],
  };
})();

Understanding the results

Summary table

Compares average timing for each phase between first-party and third-party scripts.

Timing phases

PhaseCalculationWhat 0 means
DNSdomainLookupEnd - domainLookupStartCached or CORS restricted
TCPconnectEnd - connectStart (minus TLS)Reused connection or CORS
TLSconnectEnd - secureConnectionStartHTTP (not HTTPS) or CORS
RequestresponseStart - requestStartCORS restricted
ResponseresponseEnd - responseStartAlways available
TotalresponseEnd - startTimeAlways available

CORS restrictions

Third-party scripts often show - for detailed timings because the server doesn’t send the Timing-Allow-Origin header. Only total is always available.

# Server needs to send this header for full timing data: Timing-Allow-Origin: *

Slow script detection

Scripts taking more than 500ms are highlighted with their slowest phases identified.

Typical values

PhaseGoodSlowVery Slow
DNS< 20ms20-100ms> 100ms
TCP< 50ms50-150ms> 150ms
TLS< 50ms50-150ms> 150ms
Request (TTFB)< 100ms100-300ms> 300ms
ResponseDepends on size--
Total< 200ms200-500ms> 500ms

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.

Browser support

The snippet needs these features to run.

FeatureChromeEdgeFirefoxSafari
PerformanceResourceTiming.transferSize (opens in a new tab) 54 17 45 16.4
Document.scripts (opens in a new tab) 36 12 61 10.1
All of the above 54 17 61 16.4

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

FeatureChromeEdgeFirefoxSafari
<link rel=“preconnect”> (opens in a new tab) 46 79 39 11.1
<link rel=“dns-prefetch”> (opens in a new tab) 46 79 127 5

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