DEV Community

Cover image for How to Fix Slow Page Loads Caused by Third-Party Scripts
Alan West
Alan West

Posted on

How to Fix Slow Page Loads Caused by Third-Party Scripts

We've all been there. You ship a new feature, everything's snappy in dev, and then someone adds "just one tracking pixel" and suddenly your Lighthouse score tanks. Last month I spent two days hunting down a 4-second LCP regression on a production site, and the culprit was a chain of third-party scripts I didn't even know existed.

If you've ever been blamed for slow page loads that aren't really your fault, this one's for you.

The Symptom

Your app feels fine on localhost. Network tab looks reasonable. But real users complain it's slow, and your CrUX data is screaming. Sound familiar?

Here's what usually shows up in the Performance panel:

  • LCP over 2.5s in the field
  • INP spiking when users interact
  • A Total Blocking Time number that makes you wince
  • CLS jumping right when ads or banners load

The weird part: your own JavaScript bundle is fine. You shipped 80kb gzipped and code-split everything. So what gives?

The Root Cause

Third-party scripts are the silent killer. Analytics, ad networks, A/B testing tools, chat widgets, social embeds, marketing pixels — they all execute on the main thread, and most of them load synchronously by default. Worse, many of them load more scripts after they load, creating a network waterfall that blocks rendering.

I once profiled a site that loaded 47 third-party scripts. The marketing team had no idea. Each script ran a tag manager that fired a pixel that lazy-loaded another tracker. A Russian doll of jank.

There are three specific problems you're fighting:

  1. Main thread contention — third-party JS competes with your hydration and interaction handlers
  2. Render-blocking requests — scripts loaded without async or defer block parsing
  3. Layout shift — ad slots and banners with no reserved space push content around

Step 1: Find the Actual Offenders

Before optimizing, measure. Open Chrome DevTools, go to the Performance panel, and record a cold page load with CPU throttling at 4x and network throttling at Slow 4G. Look for long tasks (the red triangles) and check which scripts are firing during them.

You can also use the Resource Timing API to spot the worst loaders:

// Quick audit in DevTools console — find scripts not from your domain
performance.getEntriesByType('resource')
  .filter(r => r.initiatorType === 'script')
  .filter(r => !r.name.includes(location.hostname))
  .map(r => ({
    url: r.name,
    // duration is round-trip time including parse + execute kick-off
    duration: Math.round(r.duration),
    sizeKb: Math.round(r.transferSize / 1024)
  }))
  .sort((a, b) => b.duration - a.duration);
Enter fullscreen mode Exit fullscreen mode

Run that and you'll get a sorted list of every third-party script with its load time. Half the time, the worst offender is something added six months ago and forgotten.

Step 2: Defer What You Can

The simplest fix is the most underused: add defer or async to script tags. defer waits until the HTML is parsed; async loads in parallel but executes whenever it's ready.

<!-- Bad: blocks the HTML parser -->
<script src="https://example.com/tracker.js"></script>

<!-- Better: doesn't block, executes after DOMContentLoaded -->
<script src="https://example.com/tracker.js" defer></script>

<!-- Best for fire-and-forget analytics -->
<script src="https://example.com/tracker.js" async></script>
Enter fullscreen mode Exit fullscreen mode

Use defer when execution order matters (multiple scripts that depend on each other) and async when each script is independent. See MDN's script element docs for the exact ordering guarantees.

Step 3: Use the Facade Pattern for Heavy Embeds

For things like video embeds, chat widgets, and social share buttons, load a lightweight placeholder first and only fetch the real thing when the user interacts.

// Replace a heavy iframe embed with a click-to-load facade
const facade = document.querySelector('.video-facade');
facade.addEventListener('click', () => {
  const iframe = document.createElement('iframe');
  iframe.src = facade.dataset.embedUrl;
  iframe.allow = 'autoplay; encrypted-media';
  // Swap the static thumbnail for the real iframe
  facade.replaceWith(iframe);
}, { once: true });
Enter fullscreen mode Exit fullscreen mode

I migrated three sites to this pattern last year and saw LCP drops of 1-2 seconds across the board. The win is real, and the implementation is maybe twenty lines of code per embed type.

Step 4: Move Scripts off the Main Thread

This is the nuclear option, but it works. Partytown is an MIT-licensed library that runs third-party scripts inside a web worker via proxied DOM access. Your main thread stays free for hydration and user interactions, and the trackers still fire.

The core idea:

<!-- Mark third-party scripts with this type and Partytown
     intercepts them, running them in a Web Worker -->
<script type="text/partytown" src="https://example.com/analytics.js"></script>
Enter fullscreen mode Exit fullscreen mode

I haven't tested this with every analytics provider out there, so check the Partytown compatibility notes before you rip out your existing setup. Scripts that need synchronous access to certain browser APIs can misbehave under the proxy.

Step 5: Kill Layout Shift from Ad Slots

Reserve space for dynamic content before it loads. This is the cheapest win on the list.

/* Reserve space so the ad doesn't push content around when it loads */
.ad-slot {
  min-height: 250px;
  /* contain: layout tells the browser this element's layout
     is independent — internal changes won't reflow siblings */
  contain: layout;
}

/* For responsive slots, aspect-ratio holds the box */
.ad-slot-responsive {
  aspect-ratio: 16 / 9;
  min-height: 100px;
}
Enter fullscreen mode Exit fullscreen mode

The contain: layout line is the magic one. It scopes layout calculations to the element, so a banner painting late inside doesn't shove your article body down the page.

Prevention: Build a Performance Budget

The fix is great, but stopping the problem from coming back is better. Set up a budget file and enforce it in CI with Lighthouse CI or similar tooling.

{
  "ci": {
    "assert": {
      "assertions": {
        "third-party-summary": ["error", { "maxNumericValue": 500 }],
        "total-blocking-time": ["error", { "maxNumericValue": 200 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

When someone asks to add a new script, point at the failing CI job. It's a much easier conversation than "please stop breaking our site."

A few more habits that have saved me:

  • Treat every new third-party script as a code review with a perf checklist
  • Audit your existing scripts quarterly — many get added once and never removed
  • Self-host scripts when the vendor allows it (cuts DNS and TLS overhead)
  • Use resource hints like preconnect and dns-prefetch for unavoidable third parties

Wrapping Up

Third-party script bloat is the kind of problem that creeps in slowly and then suddenly defines your user experience. The fix isn't one big change — it's a combination of deferring, lazy-loading, isolating, and budgeting.

Start with the audit snippet, find your worst offenders, and pick the technique that fits each one. You don't need to do everything at once. Even just adding defer to a few tags and reserving ad slot heights will move the needle on real-user metrics within a release cycle.

Top comments (0)