Every third-party script on your website charges a tax, and it charges every visitor on every page, whether or not the script did anything useful that day. The tax is paid in four currencies (bytes, main-thread time, privacy exposure, and accessibility), and most organizations only ever look at the first.

The pattern is familiar. Marketing adds a heatmap tool for a two-week test. Support adds chat. Someone embeds a review carousel and a video. Two years later nobody remembers who asked for what, and the page takes noticeably longer to respond to a tap than it used to. This article covers how to measure the tax per script, how to budget it, how to load what you keep more cheaply, and how to stop new scripts from arriving without an owner.

Four currencies, one invoice

Bytes. Every script has to be downloaded, parsed, and compiled, and on a mid-range phone on a rural network that competes directly with your own content. Google's threshold for a good Largest Contentful Paint is 2.5 seconds or less at the 75th percentile. A tag that delays your hero image spends that budget for you.

Main-thread time. Scripts run on the same thread that responds to clicks, taps, and key presses. Interaction to Next Paint (INP) replaced First Input Delay as a Core Web Vital on March 12, 2024, and a good INP is 200 milliseconds or less. A third-party listener attached to every click runs on your user's click, and its time counts against your score.

Privacy exposure. A script running on your page has your page's privileges. It can read the DOM, form fields, the URL, and any cookie not marked HttpOnly, and it can load further scripts from companies you have never heard of. Every tag is a decision about who sees your visitors' data.

Accessibility. Injected widgets bring their own markup. Chat launchers without accessible names, cookie banners that cover the focused element, popups that steal focus mid-form, carousels that move on their own, and iframes without titles all arrive from outside your codebase. Your audit passes in March, and the vendor ships an update in April.

Measure the tax one script at a time

Start in the lab, with Chrome DevTools and CPU throttling turned on, so a fast laptop doesn't hide the cost.

  • Performance panel. Record a page load and an interaction. The insights sidebar summarizes main-thread time and transfer size by third party, and you can dim third-party activity in the flame chart to see which work is yours.
  • Network panel. Filter to third-party requests and check the Initiator column. Scripts loaded by other scripts are your fourth parties, and nobody signed a contract with them.
  • Request blocking. Right-click a request, block its domain, and reload. Comparing the page with and without one vendor is the most persuasive measurement you can make, because it's a direct before and after.
  • Filmstrips. Run both versions through a filmstrip tool such as WebPageTest, or use the Performance panel's screenshots, and put them side by side. Executives read a filmstrip faster than a flame chart.

Lab data shows what could happen. Field data shows what your users actually got. The Long Animation Frames API attributes slow frames to the scripts that ran in them, so you can total the cost by origin across real sessions:

const costByHost = new Map();

new PerformanceObserver((list) => {
  for (const frame of list.getEntries()) {
    for (const script of frame.scripts) {
      const host = script.sourceURL ? new URL(script.sourceURL).hostname : '(inline)';
      costByHost.set(host, (costByHost.get(host) ?? 0) + script.duration);
    }
  }
}).observe({ type: 'long-animation-frame', buffered: true });

addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    navigator.sendBeacon('/rum/script-cost', JSON.stringify([...costByHost]));
  }
});

Collected over a week of traffic, that one report tends to settle the argument about which tag is slowing the site down.

Set a budget before the next request arrives

A budget turns an argument into arithmetic. Write one per page template, keep it in the repository, and check it in CI and in monitoring. The limits below are where we'd start for a content or e-commerce template on a mid-range phone. Tune them to your own field data.

Budget lineHow to measureStarting limit
Third-party JavaScript, compressedNetwork panel, third-party filter150 KB on content pages
Third-party main-thread time during loadPerformance panel, CPU throttled300 ms
Distinct third-party originsNetwork panel10
Third-party script time inside slow interactionsLong Animation Frames, field dataUnder 50 ms per interaction

The rule that makes a budget work is simple: a new script must fit, or something else leaves. The conversation that follows is a business decision, not an engineering veto.

Load what you keep more cheaply

Never block rendering. A third-party script in the <head> without async or defer stops the parser until it downloads and runs. Use defer when order matters and the script can wait for parsing to finish. Use async for independent scripts such as analytics.

Use facades for heavy widgets. Chat and video embeds are the classic cases: large bundles loaded on every page for the few visitors who use them. A facade is a lightweight stand-in that loads the real thing on first use. Make the facade a real button, and manage focus when the widget arrives:

<button type="button" id="chat-facade" class="chat-launcher">Chat with us</button>
const facade = document.getElementById('chat-facade');

facade.addEventListener('click', async () => {
  facade.textContent = 'Opening chat…';
  facade.setAttribute('aria-disabled', 'true');
  await loadScript(CHAT_WIDGET_SRC);
  window.ChatWidget.open();            // your vendor's API will have its own names
  window.ChatWidget.focusComposer?.(); // focus must land inside the panel
  facade.hidden = true;
}, { once: true });

function loadScript(src) {
  return new Promise((resolve, reject) => {
    const script = Object.assign(document.createElement('script'), { src, async: true });
    script.onload = resolve;
    script.onerror = reject;
    document.head.append(script);
  });
}

If the vendor's API can't move focus into the chat panel when it opens, that's a finding for the vendor review, not a reason to skip the facade. For video, show a thumbnail and a play button labeled with the video's title, and swap in the iframe, with a title attribute, on click.

Move collection server-side. With server-side tagging, the browser sends one first-party request, and your server forwards events to vendors with only the fields you choose. That cuts client JavaScript and gives you a single place to strip personal data. It is not a way around consent; the same rules apply to what you forward.

Gate on consent. Where your policy requires consent, don't load the script until it's given, and confirm in the Network panel that nothing fires before then. The consent banner is itself a third-party widget, so test it with a keyboard and a screen reader.

Govern scripts the way you govern vendors

Every script should have a register entry. A minimal one looks like this:

- id: session-replay
  owner: E-commerce director
  purpose: Diagnose checkout drop-off during the spring test
  loads: Checkout templates only, after consent
  data: URLs and clicks; all form inputs masked
  cost: 58 KB compressed; 140 ms main thread (lab, 4x CPU slowdown)
  accessibility: Injects no visible UI
  added: 2026-03-02
  expires: 2026-06-30

The process around it is short. A request names the owner, purpose, and data. Review covers performance against the budget, privacy against your notice, and a keyboard and screen reader pass over any interface the script injects. Approval comes with an expiry date. When the date arrives, the owner renews with evidence that the script is still used, or the tag comes out.

The expiry field does most of the work. Scripts without an end date live forever, because removing one requires someone to prove it's unused, and proving that is nobody's job. Two more habits help. Limit who can publish in your tag manager. And once a quarter, block each script in staging and see whether anyone notices.

Where to start this week

Pick your highest-traffic template, open DevTools, and block third-party domains one at a time. Note how load time and tap response change with each. That list is the first draft of your register. If you'd like the full inventory, our tag and script governance audit measures every script's cost and when it fires relative to consent, and our Core Web Vitals engineering sprints fix the templates where third-party code is costing you INP.

Sources

  1. Google, web.dev: Web Vitals
  2. Google, web.dev blog: Interaction to Next Paint is officially a Core Web Vital
  3. web.dev: Efficiently load third-party JavaScript
  4. web.dev: Identify slow third-party JavaScript
  5. web.dev: Best practices for using third-party embeds