# First and third party script timings

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

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

Diagram: Order of the network phases that precede a script response: redirect, DNS lookup, TCP connection, TLS handshake, request, then response start and end

| Phase              | What it measures                     | Why it matters                               |
| ------------------ | ------------------------------------ | -------------------------------------------- |
| **DNS Lookup**     | Time to resolve domain name          | Slow DNS affects all requests to that domain |
| **TCP Connection** | Time to establish connection         | Geographic distance, server load             |
| **TLS/SSL**        | Time for secure handshake            | Certificate chain, protocol version          |
| **Request**        | Time from request sent to first byte | Server processing time (TTFB)                |
| **Response**       | Time to download the resource        | File size, connection speed                  |
| **Total**          | End-to-end loading time              | Overall script impact                        |

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

> **Note:** **Related:** Use [First And Third Party Script Info](https://webperf-snippets.net/Loading/First-And-Third-Party-Script-Info.md) for a simpler overview focused on sizes and blocking status.

### Snippet

```js
// First and Third Party Script Timings 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 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((e) => !isOwnRequest(e))
    .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

| Phase    | Calculation                             | What 0 means              |
| -------- | --------------------------------------- | ------------------------- |
| DNS      | `domainLookupEnd - domainLookupStart`   | Cached or CORS restricted |
| TCP      | `connectEnd - connectStart` (minus TLS) | Reused connection or CORS |
| TLS      | `connectEnd - secureConnectionStart`    | HTTP (not HTTPS) or CORS  |
| Request  | `responseStart - requestStart`          | CORS restricted           |
| Response | `responseEnd - responseStart`           | Always available          |
| Total    | `responseEnd - startTime`               | Always 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

| Phase          | Good            | Slow      | Very Slow |
| -------------- | --------------- | --------- | --------- |
| DNS            | < 20ms          | 20-100ms  | > 100ms   |
| TCP            | < 50ms          | 50-150ms  | > 150ms   |
| TLS            | < 50ms          | 50-150ms  | > 150ms   |
| Request (TTFB) | < 100ms         | 100-300ms | > 300ms   |
| Response       | Depends on size | -         | -         |
| Total          | < 200ms         | 200-500ms | > 500ms   |

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

### 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 |
| [Document.scripts](https://developer.mozilla.org/docs/Web/API/Document/scripts)                                             |  ✅ 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.

| Feature                                                                                                         | Chrome | Edge | Firefox | Safari |
| --------------------------------------------------------------------------------------------------------------- | :----: | :--: | :-----: | :----: |
| [\<link rel="preconnect">](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/preconnect)     |  ✅ 46  | ✅ 79 |   ✅ 39  | ✅ 11.1 |
| [\<link rel="dns-prefetch">](https://developer.mozilla.org/docs/Web/HTML/Reference/Attributes/rel/dns-prefetch) |  ✅ 46  | ✅ 79 |  ✅ 127  |   ✅ 5  |

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

### Further reading

- [Resource Timing API](https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API) | MDN
- [Timing-Allow-Origin Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin) | MDN
- [ResourceTiming Visibility](https://nicj.net/resourcetiming-visibility-third-party-scripts-ads-and-page-weight/) | Nic Jansma
