Over 94% of websites load third-party scripts. And according to Chrome's research, the worst offenders block your main thread for up to 1.6 seconds—on more than half of sites analyzed.
That's not a minor performance hit. That's the difference between passing and failing Core Web Vitals. Between ranking and not ranking. Between conversions and bounces.
This guide shows you how to find which scripts are hurting your site, and exactly how to fix them—from quick wins like async and defer to modern solutions like Partytown and server-side tracking.
Why third-party scripts hurt so much
Third-party scripts impact all three Core Web Vitals:
LCP (Largest Contentful Paint): Scripts compete for bandwidth and CPU. If your browser is busy downloading and executing analytics code, your hero image loads later. Chrome found that YouTube embeds alone block the main thread for 4.5 seconds on 10% of mobile sites.
INP (Interaction to Next Paint): Every script running on the main thread delays user interactions. Click a button while Facebook Pixel is executing? The browser queues your click until the script finishes. The HTTP Archive 2024 report found third-party scripts are the main cause of poor INP scores.
CLS (Cumulative Layout Shift): Ad networks and chat widgets that inject content without reserved space cause layout shifts. A banner that loads late and pushes content down directly hurts CLS.
The compounding problem: most sites don't have one third-party script. They have ten. Google Analytics, Tag Manager, Facebook Pixel, Hotjar, Intercom, cookie consent, and more—all fighting for the main thread simultaneously.
Find your worst offenders
Before fixing anything, identify which scripts are actually causing problems.
Lighthouse audit
Run Lighthouse and look for "Reduce the impact of third-party code." This audit lists every third-party, showing:
- Main thread blocking time — How long each script blocks interactions
- Transfer size — How much data it downloads
Lighthouse flags the audit when third-party code blocks the main thread for more than 250ms total.
Chrome DevTools
For deeper analysis:
- Open DevTools → Performance tab
- Record a page load
- Look at the Main thread flame chart
- Enable "Dim 3rd parties" (top of panel) to gray out third-party activity
This shows exactly when third-party scripts execute and how long they block.
WebPageTest
For the most detailed breakdown:
- Run your URL on webpagetest.org
- Check the "Third Parties" view
- Look at "CPU Time" and "Blocking Time" columns
WebPageTest also shows the request chain—which scripts load other scripts, revealing hidden costs.
The worst offenders (with data)
Based on Chrome's research and HTTP Archive data, these categories cause the most damage:
Tag managers (when bloated)
Google Tag Manager itself is lightweight. The problem is what's inside:
Empty GTM container: ~28KB, minimal impact
Typical GTM container: 100-500KB+ depending on tags
Bloated GTM: Multiple analytics, pixels, and custom HTML
A real-world test showed removing one misconfigured tag dropped page load from 6.5 seconds to 3.4 seconds—no code changes, just tag cleanup.
The fix: Audit your GTM container quarterly. Remove tags nobody remembers adding. Delay non-critical tags until after page load.
Chat widgets
Chat widgets look small but load enormous amounts of JavaScript:
| Widget | JS Downloaded | Main Thread Impact |
|---|---|---|
| Zendesk | 500KB (2.3MB unzipped) | High |
| Drift | 200-400KB | High |
| Intercom | ~150KB | Medium (well-optimized) |
| Crisp | ~100KB | Low |
Zendesk downloads over 500KB of JavaScript—a full React application—just to render a button with a speech bubble icon.
The fix: Use a facade (fake button that loads the real widget on click) or lazy load on user interaction.
Analytics and tracking pixels
Every pixel adds requests and JavaScript execution:
- Google Analytics 4: Relatively lightweight (~30KB)
- Facebook/Meta Pixel: ~170KB, 4 HTTP requests
- Hotjar/FullStory: Session replay requires continuous recording
Stacking multiple pixels adds up fast. Five tracking pixels can add 2-4 seconds to every page load.
The fix: Use server-side tracking (Conversions API) or run through Cloudflare Zaraz.
Video embeds
YouTube embeds are surprisingly expensive:
"YouTube embeds block the main thread for 4.5 seconds for 10% of the websites on mobile, and at least 1.6 seconds for 50% of the websites studied." — Chrome DevRel
The fix: Use lite-youtube-embed or load a thumbnail that swaps for the real player on click.
Quick fixes: async and defer
The simplest improvement: stop scripts from blocking HTML parsing.
Default behavior (bad)
<!-- Blocks everything until downloaded and executed -->
<script src="https://example.com/script.js"></script>
When the browser hits this tag, it stops parsing HTML, downloads the script, executes it, then continues. Your page freezes.
Async (better)
<script async src="https://example.com/analytics.js"></script>
The browser downloads the script in parallel with HTML parsing, but pauses parsing to execute when the download finishes. Scripts run in load-first order, not document order.
Best for: Analytics, ads, and scripts that don't depend on other scripts or DOM content.
Defer (usually best)
<script defer src="https://example.com/non-critical.js"></script>
The browser downloads in parallel and waits until HTML parsing is complete before executing. Scripts run in document order.
Best for: Scripts that need the DOM, scripts that depend on each other, anything non-critical.
When to use which
<!-- Critical analytics - load early but don't block -->
<script async src="/analytics.js"></script>
<!-- Chat widget - not needed until page is ready -->
<script defer src="/chat-widget.js"></script>
<!-- Multiple dependent scripts - maintain order -->
<script defer src="/library.js"></script>
<script defer src="/app-that-uses-library.js"></script>
Important: async and defer only work on external scripts. They have no effect on inline scripts.
Delay loading until interaction
For scripts users might not need, wait until they interact:
Chat widget example
// Don't load Intercom until user shows intent
let intercomLoaded = false;
function loadIntercom() {
if (intercomLoaded) return;
intercomLoaded = true;
// Load the real Intercom script
const script = document.createElement('script');
script.src = 'https://widget.intercom.io/widget/YOUR_APP_ID';
document.body.appendChild(script);
}
// Load on click
document.querySelector('.chat-button').addEventListener('click', loadIntercom);
// Or load after idle time
if ('requestIdleCallback' in window) {
requestIdleCallback(loadIntercom, { timeout: 5000 });
} else {
setTimeout(loadIntercom, 5000);
}
Facade pattern for chat widgets
Show a fake button that looks identical, load the real widget on interaction:
<button class="fake-chat-button" onclick="loadRealChat()">
💬 Chat with us
</button>
<script>
function loadRealChat() {
// Hide fake button
document.querySelector('.fake-chat-button').style.display = 'none';
// Load and show real widget
window.Intercom('boot', { app_id: 'YOUR_APP_ID' });
window.Intercom('show');
}
</script>
For React apps, the react-live-chat-loader package handles this pattern for Intercom, Drift, Messenger, and others.
Delay GTM tags
In Google Tag Manager, change triggers from "All Pages" to "Window Loaded" or custom events:
// Fire a custom event after 3 seconds
setTimeout(() => {
window.dataLayer.push({ event: 'delayed_load' });
}, 3000);
Then trigger non-critical tags (Hotjar, remarketing pixels) on delayed_load instead of page view.
Modern solutions: Partytown and web workers
The fundamental problem: all JavaScript runs on one thread. When third-party scripts execute, your code waits.
Partytown solves this by moving third-party scripts to a web worker—a separate thread that doesn't block your UI.
How Partytown works
<!-- Without Partytown: blocks main thread -->
<script src="https://www.googletagmanager.com/gtm.js"></script>
<!-- With Partytown: runs in web worker -->
<script type="text/partytown" src="https://www.googletagmanager.com/gtm.js"></script>
Scripts with type="text/partytown" don't execute normally. Partytown intercepts them, runs them in a web worker, and proxies DOM access back to the main thread.
Real results
Chrome's research showed:
"Moving the execution of the GTM container and its associated tag scripts to a web worker reduced TBT by 92%."
A comparison showed Lighthouse scores jumping from 70 to 99 after implementing Partytown.
Next.js implementation
npm install @builder.io/partytown
// next.config.js
const { withPartytown } = require('@builder.io/partytown/next');
module.exports = withPartytown({
partytown: {
forward: ['dataLayer.push'],
},
});
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
import { Partytown } from '@builder.io/partytown/react';
export default function Document() {
return (
<Html>
<Head>
<Partytown forward={['dataLayer.push']} />
<script
type="text/partytown"
dangerouslySetInnerHTML={{
__html: `/* GTM script here */`,
}}
/>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
For more Next.js optimization, see our Next.js Core Web Vitals guide.
Nuxt implementation
npm install @nuxt/scripts
// nuxt.config.ts
export default defineNuxtConfig({
scripts: {
globals: {
googleTagManager: {
id: 'GTM-XXXXX',
partytown: true, // Run in web worker
},
},
},
});
See our Nuxt performance guide for more optimizations.
Partytown trade-offs
Works well for:
- Google Tag Manager
- Google Analytics
- Facebook Pixel
- Most analytics and tracking scripts
May have issues with:
- Scripts that need synchronous DOM access
- Scripts that use
document.write - Some older ad networks
Always test thoroughly in staging before deploying.
Server-side solutions
The ultimate fix: don't run third-party code in the browser at all.
Cloudflare Zaraz
If you're on Cloudflare, Zaraz replaces GTM with server-side execution:
- Enable Zaraz in your Cloudflare dashboard
- Add tools (GA4, Facebook Pixel, etc.) through Zaraz's interface
- Remove client-side scripts entirely
Zaraz runs over 50 tools server-side, sending data to platforms without any browser JavaScript.
Real results from Instacart:
- Total Blocking Time: 500ms → 0ms
- Time to Interactive: 11.8s → 4.26s (63% improvement)
- CPU Time: 3.62s → 1.45s (60% improvement)
Facebook Conversions API
Replace Facebook Pixel with server-side events:
// Server-side (Node.js example)
const bizSdk = require('facebook-nodejs-business-sdk');
const eventRequest = new bizSdk.EventRequest(
ACCESS_TOKEN,
PIXEL_ID
);
eventRequest.setEvents([
new bizSdk.ServerEvent()
.setEventName('Purchase')
.setEventTime(Math.floor(Date.now() / 1000))
.setUserData(userData)
.setCustomData(customData)
]);
await eventRequest.execute();
This eliminates the 170KB Facebook Pixel JavaScript entirely while maintaining conversion tracking.
Google Analytics Measurement Protocol
Send events server-side instead of using gtag.js:
// Server-side
fetch('https://www.google-analytics.com/mp/collect?' + new URLSearchParams({
measurement_id: 'G-XXXXX',
api_secret: 'YOUR_SECRET',
}), {
method: 'POST',
body: JSON.stringify({
client_id: 'CLIENT_ID',
events: [{
name: 'purchase',
params: { value: 99.99 }
}]
})
});
Decision framework
Which solution should you use? Here's a flowchart:
Is the script critical for page render?
├─ Yes → Must load synchronously (rare)
└─ No → Continue
Does the script need to run on every page?
├─ No → Load only on pages that need it
└─ Yes → Continue
Can you use a server-side alternative?
├─ Yes (Zaraz, Conversions API) → Use it, zero browser impact
└─ No → Continue
Is the script compatible with Partytown?
├─ Yes → Use Partytown for web worker execution
└─ No → Continue
Can the script be delayed until interaction?
├─ Yes → Use facade pattern or idle callback
└─ No → Use defer, avoid async unless needed
Performance budget for third-parties
Set limits and enforce them:
// Track third-party size in your build
const THIRD_PARTY_BUDGET = {
totalSize: 200 * 1024, // 200KB max
mainThreadTime: 250, // 250ms max blocking
scriptCount: 5, // Max 5 third-party scripts
};
Use Lighthouse CI or SpeedCurve to fail builds that exceed budgets.
Checklist
Audit (do first)
- Run Lighthouse "third-party code" audit
- List every third-party script and its purpose
- Remove scripts nobody can justify
Quick wins
- Add
asyncordeferto all third-party scripts - Move GTM tags from "All Pages" to "Window Loaded"
- Implement facades for chat widgets
Bigger improvements
- Set up Partytown for GTM and analytics
- Enable Cloudflare Zaraz if available
- Implement Facebook Conversions API
- Replace YouTube embeds with lite-youtube
Ongoing
- Set performance budgets for third-party code
- Audit GTM container quarterly
- Monitor third-party impact in Core Web Vitals reports
What's next
Third-party scripts are often the biggest Core Web Vitals blocker—but they're also the most fixable without touching your application code.
Start by auditing. Run Lighthouse, find your worst offenders, and pick one to fix. Even moving from default script loading to defer can save hundreds of milliseconds.
For framework-specific optimizations:
- Next.js Core Web Vitals
- Nuxt Performance Optimization
- React Performance Optimization
- Vite + React Performance
Still struggling with Core Web Vitals? Run your site through SpeedFix for personalized, framework-specific fixes.