Your third-party JavaScript transfer size is creeping upward release after release, and no single change looks big enough to argue about — but the cumulative weight is now the largest slice of your payload.

Triage: Measure Where the Bytes Are Coming From

Before capping anything, quantify the current third-party transfer size and attribute it to specific vendors. A budget set without measurement is either toothless or a constant false alarm.

  1. Run Lighthouse and open the Reduce the impact of third-party code audit (third-party-summary). It lists each third-party entity with its transfer size and main-thread blocking time, sorted by impact.
  2. Cross-check with the Chrome DevTools Coverage tab (open the Command Menu, run “Show Coverage”, reload). It shows how many of each script’s bytes are actually executed — a vendor shipping 180 KB of which 15% runs is a prime budget target.
  3. In the Network panel, filter to 3rd-party (via the request-blocking/third-party filter) and sort by the Transfer column. Confirm you are reading the transfer (compressed, over-the-wire) size, not the larger uncompressed resource size.
  4. Record the total third-party transfer size and the top three contributors. These numbers set your initial ceiling.

The Coverage tab is the most persuasive artifact in this triage because it separates shipped bytes from useful bytes. A session-replay recorder that transfers 160 KB but executes only a fraction on a typical page is not merely heavy — it is heavy for no rendered benefit, which makes it the obvious first candidate to defer, lazy-load, or drop. Note that Coverage reports uncompressed executed bytes, whereas your budget is denominated in transfer (compressed) size, so use Coverage to rank offenders and the Network Transfer column to size the budget. Capture the numbers on a cold load with the cache disabled, since a warm cache reports near-zero transfer and will mislead you into setting a ceiling far below reality.

Root Cause: No Size Ceiling on Cumulative Third-Party Payload

Third-party transfer size creeps because each vendor is added in isolation and nothing measures the sum. A tag manager, a session-replay recorder, two ad pixels, and a customer-support widget each look reasonable alone, but together they can dwarf your first-party bundle — and unlike first-party code, their size is set by the vendor and changes without a commit in your repository when they push a new SDK version.

There is a second, subtler driver: vendor SDK version drift. Because a third-party script is fetched from the vendor’s CDN, its size can grow without any change in your repository. A tag that was 40 KB when you integrated it can silently become 70 KB after the vendor ships a feature, and no code review will ever catch that — there is no diff. Only a ceiling measured on every build catches vendor-side inflation, which is precisely the class of regression that slips past ordinary review.

The remedy is a declared ceiling on the third-party resource type in a Lighthouse budget.json, enforced in CI so any breach fails the build. Transfer-size budgets are a resourceSizes entry within the broader budget model described in Enforcing Performance Budgets with Lighthouse CI; this page focuses only on sizing and shaping that ceiling for third-party bytes. Byte weight also compounds with request scheduling — the same payload arriving on a cold connection costs more wall-clock time, which is why capping bytes pairs naturally with optimizing the network waterfall for external assets.

Two ceilings are better than one here, because bytes and request count fail in different ways and a single limit lets one of them drift. A budget on bytes alone permits a steadily growing number of small tags; a budget on count alone permits one tag to double in size.

Cap the bytes and the count Two axes. Capping transfer size alone allows the number of third-party requests to grow steadily, each one adding connection overhead and main-thread work that bytes do not capture. Capping request count alone allows a single existing tag to double in size without breaching anything. Capping both closes each gap, and the recommended pairing is a total byte ceiling with a separate ceiling on distinct third-party origins. bytes only misses: more and more small tags, each adding a handshake and a task the count creeps invisibly count only misses: one existing tag doubling in size without adding a request the usual vendor-update failure both, plus origins third-party bytes ≤ 500 KB third-party requests ≤ 25 distinct origins ≤ 6 origins is the one that hurts most The origin ceiling is the least common and the most protective: each new origin is a full handshake. It is also the one a vendor cannot breach silently — a new origin is visible in a single Network filter.

Resolution: A budget.json That Caps Third-Party Bytes and Count

Create budget.json at the repository root. The third-party entry caps the aggregate compressed weight of every cross-origin vendor request; pairing it with a resourceCounts cap stops teams from evading the size limit by adding many small tags:

[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "third-party", "budget": 180 },
      { "resourceType": "script", "budget": 350 }
    ],
    "resourceCounts": [
      { "resourceType": "third-party", "budget": 10 }
    ]
  }
]

Every budget value in resourceSizes is kilobytes of transfer (compressed) size, matching the Transfer column in DevTools. The 180 ceiling means “no more than 180 KB of compressed third-party payload across the page”; the resourceCounts cap of 10 means “no more than ten third-party requests.”

To hold one specific vendor accountable rather than the whole bucket, add a per-host budget object. Lighthouse budgets support a budgets array keyed by host pattern, so you can cap a single origin independently:

[
  {
    "path": "/*",
    "resourceSizes": [
      { "resourceType": "third-party", "budget": 180 }
    ],
    "resourceCounts": [
      { "resourceType": "third-party", "budget": 10 }
    ]
  },
  {
    "path": "/checkout/*",
    "resourceSizes": [
      { "resourceType": "third-party", "budget": 90 }
    ]
  }
]

The second object applies a tighter 90 KB third-party ceiling to conversion-critical /checkout/* routes while the first object’s 180 KB ceiling governs everything else. Lighthouse applies the most specific matching path to each audited URL, so a checkout page is held to the stricter limit automatically.

Choose the ceiling with a little headroom above today’s measured value, but not so much that regressions hide inside the slack. If your page currently transfers 150 KB of third-party payload, a ceiling of 180 KB tolerates minor vendor drift while still catching the addition of a new tag; a ceiling of 400 KB would let the payload nearly triple before firing, defeating the point. Ratchet the number downward over time: once you have trimmed a vendor, lower the budget to lock in the win so the reclaimed bytes cannot quietly return. Treat the budget as a one-way ratchet rather than a line that only ever moves up when a vendor is added.

A budget also gives you leverage in vendor conversations. When a tag manager or ad SDK breaches the ceiling, the failing CI check is objective evidence you can take to the vendor or to a build-versus-buy decision, rather than a subjective complaint that “the site feels slow.” The ceiling turns an abstract worry about third-party weight into a concrete, enforced contract.

Because the budget is measured against transfer size, the compression you serve directly affects headroom. A vendor script that is 180 KB under gzip may be 150 KB under Brotli, so serving Brotli where possible buys real room under the ceiling — but remember Lighthouse weighs whatever compression the vendor’s CDN applies to their own script, which you do not control. That is another reason to budget the aggregate third-party bucket rather than assume each vendor is optimally compressed.

Wire the file into your Lighthouse run through settings.budgetsPath and assert on the performance-budget audit so a breach fails CI:

{
  "ci": {
    "collect": {
      "settings": { "budgetsPath": "./budget.json" }
    },
    "assert": {
      "assertions": {
        "performance-budget": ["error", { "minScore": 1 }]
      }
    }
  }
}

Budgets in budget.json can be scoped by path pattern, which matters because a realistic site does not have one third-party profile. A marketing landing page and an authenticated dashboard legitimately carry different vendors, and a single global ceiling forces the tighter page to carry the looser page’s allowance.

One ceiling per page type, not one per site Three path patterns with different third-party allowances. Marketing landing pages carry advertising and testing vendors and are allowed the largest budget. Article pages carry analytics and an embed and are allowed a middling budget. The authenticated dashboard should carry almost no third parties at all and is given the tightest budget, which also makes any new vendor there fail immediately. /campaigns/* ads + testing + analytics · 600 KB /articles/* analytics + one embed · 350 KB /app/* error tracking only · 80 KB A single site-wide ceiling means the dashboard silently inherits the campaign page's allowance. The tight one is the valuable one: any new vendor on an authenticated page should have to argue its case.

Verification: The Budget Audit Turns Red on Breach

Verification here is not a subjective “the page feels lighter” — it is a specific row in a specific table moving to the correct side of a number you declared. Run Lighthouse against the built site with the budget attached and confirm the third-party row appears in the Budgets table with a size over/under indicator:

npx lighthouse http://localhost:4173/ --budget-path=./budget.json --view

In the report’s Budgets section, the Third-party row shows the observed transfer size and the amount over budget (for example, Third-party 204 KB +24 KB). The performance-budget audit scores 1 only when every row is within its ceiling; a single over-budget row drops the score below 1, which is why the assertion ["error", { "minScore": 1 }] fails the build on any breach. To prove the CI gate fires, temporarily drop the third-party budget to 1 and run npx lhci autorun — it must exit non-zero with a performance-budget failure that names third-party bytes. Restore the real ceiling, rerun, and confirm the audit passes with the row now within budget. The single concrete signal that the fix works: the observed third-party transfer size in the Budgets table sits at or below your declared KB ceiling, and lhci autorun exits 0.

Compressed, not uncompressed

One measurement detail decides whether the budget means anything: transferSize is what crossed the wire, decodedBodySize is what the browser ended up with, and they differ by a factor of three or four for text. Budgets set against the wrong one are either trivially loose or impossible.

Two sizes, two different budgets The same 120 kilobyte compressed script decodes to roughly 420 kilobytes. Transfer size is the correct basis for a network budget, since it is what the visitor's connection actually carries. Decoded size correlates better with parse and compile cost on the main thread, so it is the better basis for a blocking-time budget. A note records that a zero transfer size means the response came from cache and should be excluded rather than counted as free. transferSize 120 KB on the wire → budget the network with this decodedBodySize 420 KB parsed and compiled → blocking The same script, measured two legitimate ways. A budget quoting one against the other is meaningless. A transferSize of zero means a cache hit — exclude those rows rather than recording the vendor as free. Cross-origin entries report zero for both unless the vendor sends Timing-Allow-Origin.

Common Pitfalls

  • Budgeting uncompressed size by mistake. resourceSizes budgets are compared against transfer (compressed) size. If you sized the ceiling from the uncompressed bytes shown in the Sources panel, the budget will be far too loose. Read the value from the Network panel’s Transfer column instead.
  • Capping size but not count. A third-party size ceiling alone lets a team add ten tiny pixels that each dodge the byte limit while multiplying connections and main-thread work. Always pair resourceSizes with a resourceCounts cap on third-party.
  • Assuming subdomains are always third-party. Lighthouse classifies by registrable domain via its entity map, so cdn.yoursite.com usually counts as first-party and will not appear in the third-party bucket. If a genuinely external CDN is misclassified, target it with an explicit host-pattern budget rather than relying on the aggregate third-party type.
  • Measuring a lazily loaded vendor that never fires in the synthetic run. A tag injected only after a consent grant, an idle callback, or a scroll interaction may not download during a headless Lighthouse run, so its bytes never count against the budget even though real users pay for them. If a vendor loads conditionally, script the interaction in your collect step or budget it from field data instead, so the ceiling reflects the payload users actually receive.

Frequently Asked Questions

Should the budget cover all third parties together or one per vendor?

Both, for different purposes. A single aggregate ceiling is what protects the visitor, since their connection does not care how the bytes are divided — and it creates the useful dynamic where adding a vendor requires removing or shrinking another, which is a conversation worth having explicitly rather than by accident.

Per-vendor budgets are for attribution rather than protection. They tell you which line to look at when the aggregate moves, and they let you set a tighter expectation on a vendor with a history of growth. Derive them from the same registry that drives your RUM attribution so the two sets of numbers are comparable.

What is a realistic starting ceiling?

Whatever you currently ship, minus a little. A budget set from an aspirational number fails on day one and gets disabled; a budget set at your current measured total with a small reduction fails only when something grows, which is the behaviour you actually want from a gate.

Ratchet it down deliberately after that. Reducing the ceiling by a few percent each quarter, as a scheduled change rather than a reaction, turns the budget into a mechanism for improvement instead of a line that only ever moves upward when someone needs it to.


Up: Enforcing Performance Budgets with Lighthouse CI