Auditing Third-Party Scripts With Lighthouse
You cannot budget what you have not attributed. "The page is slow" becomes actionable the moment you can say "the chat widget is 340 ms of the 940 ms of main-thread time, and the tag manager is another 120 ms with an empty container". Lighthouse produces exactly that attribution, and it can run in CI so the numbers do not drift back.
This guide runs the audit end to end: the third-party summary for attribution, the treemap for what is inside each bundle, a long-task trace for costs that appear after load, and a CI gate that fails the build when the total crosses a budget. It is the measurement companion to Third-Party Script Performance on Static Sites.
Prerequisites
- Node 20+ with
lighthouseavailable vianpx, or Chrome's built-in Lighthouse panel for interactive runs. - A preview URL per pull request — the pattern in Preview Environments for Pull Requests is what makes per-PR auditing possible.
- A representative page: whichever template carries the most third-party code, not the homepage by default.
Step 1 — Attribute the Cost by Entity
The third-party-summary audit groups every request by the entity that owns it and reports both transfer size and main-thread blocking time:
npx lighthouse https://preview.example.com/docs/deploying/ \
--form-factor=mobile --throttling-method=simulate \
--only-audits=third-party-summary,total-byte-weight,bootup-time \
--output=json --output-path=./lh.json --quiet
jq -r '.audits["third-party-summary"].details.items[]
| [.entity, (.transferSize/1024|floor|tostring + " KB"),
(.blockingTime|floor|tostring + " ms")] | @tsv' lh.json
Sort the output by blocking time, not by size. The ranking almost always differs, and blocking time is what a reader feels.
Run it on mobile with simulated throttling even if your audience skews desktop. The mobile profile models a mid-tier device, and third-party code is precisely the category whose cost scales with how slow the CPU is: a bundle that parses and executes in 40 ms on a laptop routinely takes 300 ms on a phone from three years ago. Auditing on desktop hardware produces a report where every vendor looks affordable.
Step 2 — Open the Treemap for What Is Inside
The treemap (--view in the CLI, or the Treemap link in the HTML report) breaks each bundle into modules and marks unused bytes. It answers a different question from the summary: not "who is expensive" but "what part of this is expensive, and are we even using it".
npx lighthouse https://preview.example.com/docs/deploying/ \
--form-factor=mobile --output=html --output-path=./lh.html --view
Two patterns show up constantly on content sites. A vendor bundle that is 70-80% unused, because the site enables one of its twelve features — a candidate for a lighter alternative or a self-hosted replacement. And a polyfill bundle shipped to every browser to satisfy one that nobody in the analytics uses any more.
Step 3 — Trace the Costs That Appear After Load
Lighthouse stops recording after the run, so anything periodic is invisible to it. Record a Performance trace with the page idle for thirty seconds and read the Bottom-Up view grouped by URL, or collect it programmatically:
// Long tasks with attribution, logged for 30 s
const seen = new Map();
new PerformanceObserver((list) => {
for (const t of list.getEntries()) {
if (t.duration < 50) continue;
const src = t.attribution?.[0]?.containerSrc || t.attribution?.[0]?.name || 'unknown';
seen.set(src, (seen.get(src) || 0) + t.duration);
}
}).observe({ type: 'longtask', buffered: true });
setTimeout(() => console.table([...seen].map(([src, ms]) => ({ src, ms: Math.round(ms) }))), 30000);
Anything that appears here while the page is idle is a recurring cost paid for the whole session — a presence poll, a heartbeat, a re-render on focus. These are the costs that damage Interaction to Next Paint without touching any load metric.
Step 4 — Gate the Budget in CI
An audit that runs once is a snapshot; an audit that runs on every pull request is a ratchet. Lighthouse CI's assertions read the same audits:
{
"ci": {
"collect": { "url": ["https://preview.example.com/docs/deploying/"], "numberOfRuns": 3 },
"assert": {
"assertions": {
"third-party-summary": ["error", { "maxNumericValue": 150 }],
"total-byte-weight": ["error", { "maxNumericValue": 512000 }],
"interactive": ["warn", { "maxNumericValue": 3500 }]
}
}
}
}
# .github/workflows/perf.yml
- name: Lighthouse budget
run: npx --yes @lhci/cli autorun --config=./lighthouserc.json
Three runs and a median matter here: a single Lighthouse run on a shared CI runner varies enough to fail a passing build. The same discipline applies to any performance gate, as covered in Measuring Build-Time Regressions in CI.
Step 5 — Turn the Report Into an Owned List
A report nobody owns changes nothing. Convert the ranked output into a table in the repository, one row per entity, with an owner and a decision. The decision column has only four values, and forcing every vendor into one of them is what makes the audit finish rather than repeat.
The bottom-right quadrant is the easiest and most contested. An empty tag manager container costing 120 ms is trivially removable in engineering terms and often defended on the grounds that someone might need it later. Bring the number: 120 ms on every page view, for a capability nobody has used in eleven months, is a concrete trade rather than a preference. Keeping a record of when each vendor was last actually used makes that conversation short.
Re-run the audit after each change and update the row. The list is finished when every entity has an owner, a decision and a measured cost — not when the score reaches a particular number.
Measured Impact
Running the audit on a documentation site's article template and acting on the top two rows:
| Stage | Third-party blocking | Transfer | INP | Lighthouse perf |
|---|---|---|---|---|
| Before the audit | 760 ms | 1.28 MB | 240 ms | 61 |
| Chat widget interaction-gated | 420 ms | 1.18 MB | 165 ms | 74 |
| Video replaced with a facade | 110 ms | 430 KB | 130 ms | 92 |
| Tag manager removed, analytics self-hosted | 22 ms | 380 KB | 120 ms | 97 |
Pitfalls & Rollback
- Auditing the homepage only. Article and documentation templates usually carry more third-party code than the marketing front page.
- Single-run assertions. Lighthouse on a shared runner is noisy; use three runs and the median or you will chase phantom regressions.
- Chasing the composite score. It hides which metric moved. Assert on the underlying numbers.
- Ignoring the tag manager container. Its own bundle may be small while the tags inside it are not — audit production, where the real container is live.
- Auditing a warm cache. Run with a fresh profile; a cached vendor bundle understates the first-visit cost that new readers pay.
- Rollback: the audit changes nothing by itself. The CI assertion is one config file — lowering the threshold or removing the assertion restores the previous pipeline immediately.
Conclusion
Attribution turns an argument about performance into a ranked list. Run the third-party summary against the template that carries the most vendor code, sort by blocking time, use the treemap to see what is inside the worst offenders, trace an idle page for recurring costs, and then freeze the result with a Lighthouse CI assertion. The fixes themselves — gating, facades, self-hosting — are covered in Third-Party Script Performance on Static Sites.
FAQ
What is the difference between transfer size and blocking time in the report?
Transfer size is what came down the wire; blocking time is how long the main thread was unavailable to respond to input because that script was executing. They correlate loosely at best — a small script that builds UI can block for longer than a large one that only sends a request.
Why does the treemap show different numbers from the network panel?
The treemap reports unminified script bytes and, when source maps are available, attributes them to modules, so it answers "what is inside this bundle". The network panel reports compressed transfer. Use the treemap to find unused code and the network panel to budget bandwidth.
Should I audit a preview deploy or production?
Both, for different reasons. Audit a preview deploy on every pull request to catch regressions before they ship. Audit production monthly, because tag managers and vendor updates change what runs without any change to your repository.
How do I attribute a long task to a specific vendor?
Record a Performance trace, open the Bottom-Up view and group by URL. Anything attributed to a script whose origin is not yours is third-party work, and the attribution field on long-task entries gives you the same mapping programmatically.
Is a Lighthouse score a good performance budget?
No. The score is a weighted composite that hides which metric moved, and it is noisy run to run. Budget the underlying numbers you care about — third-party blocking time, transfer size, LCP and INP — and treat the score as a headline only.
Related
- Parent: Third-Party Script Performance on Static Sites — what to do with what the audit finds.
- Lazy-Loading YouTube Embeds on Static Sites — usually the second row of the ranking.
- Self-Hosting Analytics to Cut Third-Party Requests — removing an origin entirely.
- Preview Environments for Pull Requests — the URL the CI audit runs against.
- JavaScript Hydration & Partial Rendering — auditing your own bundle with the same tools.