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
| 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)
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
| 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.
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 (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.
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| PerformanceResourceTiming.renderBlockingStatus (opens in a new tab) | 107 | 107 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.