Third-party impact by domain
Groups every third-party request by root domain and reports, per domain, the request count, the transfer size, whether any of its requests blocks rendering, and the main-thread time its scripts take inside long animation frames. It answers which third party to fix first.
Why this matters
A page rarely suffers from third parties as a group. A few domains usually cause most of the damage: one tag manager that blocks rendering, one widget that runs 300 ms of script. Ranking domains by impact turns a long list of requests into a short list of decisions: remove, defer, self-host or replace.
Snippet
// Third-Party Impact by Domain
// Groups third-party requests by root domain and reports requests, transfer size, render-blocking and main-thread time
// https://webperf-snippets.nucliweb.net
(() => {
const MAX_ITEMS = 50;
const LOAF_WARNING_MS = 250; // Script time in long animation frames above which one domain is a problem
const MAX_LISTED_DOMAINS = 5;
// 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 = [];
// @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
// A size that misses some resources is a lower bound, and one that misses all of them is unknown
const sizeLabel = (bytes, unknown, total) => {
if (total > 0 && unknown === total) return "unknown";
return (unknown > 0 ? "≥ " : "") + formatBytes(bytes);
};
// @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) {
return getRootDomain(hostname) === getRootDomain(location.hostname);
}
// @end-shared isFirstParty
const ownRoots = new Set(
OWN_DOMAINS.map((d) => getRootDomain(String(d).trim().toLowerCase().replace(/^[a-z]+:\/\//, "").split("/")[0]))
);
// Root domain of an http(s) URL, or null for anything else (data:, blob:, empty)
const rootDomainOf = (url) => {
try {
const u = new URL(url);
return /^https?:$/.test(u.protocol) ? { root: getRootDomain(u.hostname), first: isFirstParty(u.hostname) || ownRoots.has(getRootDomain(u.hostname)) } : null;
} catch {
return null;
}
};
const domains = new Map();
const domainEntry = (root) => {
if (!domains.has(root)) {
domains.set(root, { domain: root, requests: 0, transferBytes: 0, sizeUnknownCount: 0, renderBlocking: false, loafMs: null, loafScripts: 0 });
}
return domains.get(root);
};
let firstPartyRequests = 0;
let thirdPartyRequests = 0;
let thirdPartyBytes = 0;
let sizeUnknownCount = 0;
performance.getEntriesByType("resource").forEach((entry) => {
const parsed = rootDomainOf(entry.name);
if (!parsed) return;
if (parsed.first) {
firstPartyRequests++;
return;
}
const d = domainEntry(parsed.root);
d.requests++;
thirdPartyRequests++;
// Cross-origin resources without Timing-Allow-Origin report every size as zero
const sizeKnown = entry.transferSize > 0 || entry.encodedBodySize > 0 || entry.decodedBodySize > 0;
if (sizeKnown) {
const bytes = entry.transferSize || entry.encodedBodySize || 0;
d.transferBytes += bytes;
thirdPartyBytes += bytes;
} else {
d.sizeUnknownCount++;
sizeUnknownCount++;
}
if (entry.renderBlockingStatus === "blocking" && (entry.initiatorType === "script" || entry.initiatorType === "link")) {
d.renderBlocking = true;
}
});
// Main-thread time: script time inside long animation frames, attributed by the script's source URL
const loafSupported = !!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame");
let thirdPartyLoafMs = null;
if (loafSupported) {
thirdPartyLoafMs = 0;
domains.forEach((d) => {
d.loafMs = 0;
});
performance.getEntriesByType("long-animation-frame").forEach((frame) => {
(frame.scripts || []).forEach((script) => {
const parsed = rootDomainOf(script.sourceURL);
if (!parsed || parsed.first) return;
const d = domainEntry(parsed.root);
d.loafMs = (d.loafMs || 0) + script.duration;
d.loafScripts++;
thirdPartyLoafMs += script.duration;
});
});
domains.forEach((d) => {
d.loafMs = Math.round(d.loafMs || 0);
});
thirdPartyLoafMs = Math.round(thirdPartyLoafMs);
}
// Render-blocking first, then main-thread time, then transfer size
const all = [...domains.values()].sort(
(a, b) =>
Number(b.renderBlocking) - Number(a.renderBlocking) ||
(b.loafMs || 0) - (a.loafMs || 0) ||
b.transferBytes - a.transferBytes ||
a.domain.localeCompare(b.domain)
);
const blocking = all.filter((d) => d.renderBlocking);
const heavy = all.filter((d) => (d.loafMs || 0) > LOAF_WARNING_MS);
const corsLimitedAnalysis = sizeUnknownCount > 0;
const listDomains = (list) => {
const names = list.slice(0, MAX_LISTED_DOMAINS).map((d) => d.domain);
return names.join(", ") + (list.length > MAX_LISTED_DOMAINS ? ` and ${list.length - MAX_LISTED_DOMAINS} more` : "");
};
const issues = [];
if (blocking.length > 0) {
issues.push({
severity: "warning",
message: `${blocking.length} third-party domain(s) serve render-blocking resources (${listDomains(blocking)}); load them async or defer, or self-host them`,
});
}
heavy.slice(0, MAX_LISTED_DOMAINS).forEach((d) => {
issues.push({
severity: "warning",
message: `${d.domain} scripts take ${d.loafMs} ms of main-thread time in long animation frames (over ${LOAF_WARNING_MS} ms); defer, delay or remove them`,
});
});
if (corsLimitedAnalysis) {
issues.push({
severity: "info",
message: `${sizeUnknownCount} third-party request(s) without Timing-Allow-Origin report zero sizes, so the transfer size is a lower bound`,
});
}
console.group("%c🌐 Third-Party Impact by Domain", "font-weight: bold; font-size: 14px;");
console.log(
`Third-party domains: ${all.length} | requests: ${thirdPartyRequests} | transfer: ${sizeLabel(thirdPartyBytes, sizeUnknownCount, thirdPartyRequests)} | first-party requests: ${firstPartyRequests}`
);
if (all.length > 0) {
console.table(
all.slice(0, MAX_ITEMS).map((d) => ({
Domain: d.domain,
Requests: d.requests,
Size: sizeLabel(d.transferBytes, d.sizeUnknownCount, d.requests),
"Render-blocking": d.renderBlocking ? "yes" : "no",
"Main thread": d.loafMs === null ? "n/a" : d.loafMs + " ms",
}))
);
} else {
console.log("%c✅ No third-party requests found", "color: #22c55e;");
}
if (!loafSupported) {
console.log("%cℹ️ Long Animation Frames are not supported; main-thread time is unavailable (Chrome 123+)", "color: #3b82f6;");
}
if (corsLimitedAnalysis) {
console.log("%cℹ️ Some third-party sizes are hidden (no Timing-Allow-Origin); sizes are a lower bound", "color: #3b82f6;");
}
if (OWN_DOMAINS.length === 0 && all.length > 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;"
);
}
console.groupEnd();
return {
script: "Third-Party-Impact-by-Domain",
status: "ok",
count: all.length,
corsLimitedAnalysis,
details: {
thirdPartyRequests,
thirdPartyBytes,
thirdPartyLoafMs,
firstPartyRequests,
blockingDomains: blocking.length,
loafSupported,
sizeUnknownCount,
},
items: all.slice(0, MAX_ITEMS),
issues,
};
})();
Understanding the results
A request is third party when its root domain differs from the root domain of location.hostname. Subdomains of the page's own site, such as cdn.example.com on www.example.com, are first party.
A site's own CDN on a different root domain, such as bbci.co.uk for bbc.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 = ["bbci.co.uk", "bbc.co.uk"];. Entries can be domains or URLs. While the list is empty, the snippet prints a reminder in the console.
Each entry in items describes one domain:
| Field | Description |
|---|---|
domain | Root domain of the third party |
requests | Requests to the domain |
transferBytes | Transfer size of the requests whose size is readable (encodedBodySize when transferSize is 0) |
sizeUnknownCount | Requests whose size is hidden |
renderBlocking | true when a script or stylesheet from the domain has renderBlockingStatus of blocking |
loafMs | Script time, in ms, attributed to the domain in long animation frames, or null when the browser does not support them |
loafScripts | Script executions attributed to the domain in long animation frames |
items holds at most 50 domains, sorted by impact: render-blocking domains first, then the most main-thread time, then the largest transfer. count and details cover every third-party domain.
| Field | Description |
|---|---|
count | Number of third-party domains |
details.thirdPartyRequests | Third-party requests |
details.thirdPartyBytes | Transfer size of the third-party requests with a readable size |
details.thirdPartyLoafMs | Third-party script time in long animation frames |
details.firstPartyRequests | First-party resource requests, for comparison |
details.blockingDomains | Domains with a render-blocking request |
details.loafSupported | false when the browser has no Long Animation Frames API |
details.sizeUnknownCount | Third-party requests whose size is hidden |
Issues
| Severity | When |
|---|---|
warning | One or more third-party domains serve render-blocking resources |
warning | A domain's scripts take more than 250 ms of main-thread time in long animation frames |
info | Some sizes are hidden because of a missing Timing-Allow-Origin header |
Main-thread time
The snippet reads long-animation-frame entries and adds up the duration of every entry in scripts[], grouped by the root domain of sourceURL. Only frames longer than 50 ms are recorded, and only script executions of 5 ms or more appear in scripts[], so loafMs is a lower bound of the real script cost. Inline scripts and scripts with an empty sourceURL count as first party and are left out.
Long Animation Frames need Chrome 123 or later. Elsewhere details.loafSupported is false and loafMs is null; the rest of the report still works.
The buffer holds the frames since the page started, so run the snippet after the page settled or after the interaction you want to measure.
Cross-origin resources without Timing-Allow-Origin
Browsers report every size as zero for a cross-origin resource unless its server sends a Timing-Allow-Origin header. The snippet counts those requests in sizeUnknownCount (per domain and in details), keeps them out of the byte totals, and sets corsLimitedAnalysis to true.
When corsLimitedAnalysis is true, treat transferBytes and details.thirdPartyBytes as lower bounds. Request counts, render-blocking flags and main-thread time are not affected.
Acting on the results
| Finding | Options |
|---|---|
| Render-blocking domain | Add async or defer, load it after the first paint, or self-host the file |
High loafMs | Delay the script until interaction or idle time, use a facade, or remove it |
| Many requests, low size | Consolidate tags or remove unused ones |
Large transferBytes | Lazy-load the widget or replace it with a lighter alternative |
Further reading
- Loading third-party JavaScript (opens in a new tab) | web.dev
- Efficiently load third-party JavaScript (opens in a new tab) | Chrome Developers
- Long Animation Frames API (opens in a new tab) | Chrome Developers
- First and Third Party Script Info | Lists the scripts behind each party
- Long Animation Frames Script Attribution | Attributes long frames to scripts