Third-Party Scripts
Manage third-party scripts efficiently to prevent performance degradation
Third-Party Scripts
Third-party scripts (analytics, ads, chat widgets) are often the biggest performance bottleneck. They can add seconds to page load time if not managed properly.
The Problem
Third-party scripts typically:
- Block rendering
- Execute synchronously
- Load additional resources
- Run on main thread
- Impact Core Web Vitals
Example impact:
Google Analytics: 45 KB, 200ms
Facebook Pixel: 80 KB, 300ms
Chat Widget: 120 KB, 400ms
Ad Scripts: 200 KB, 500ms
Total: 445 KB, 1400ms added to page load!Loading Strategies
1. Async Loading
<!-- ❌ BAD: Blocks parsing -->
<script src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<!-- ✅ GOOD: Async, doesn't block -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>2. Defer to After Page Load
// Load after page is interactive
useEffect(() => {
// Wait for page load
if (document.readyState === 'complete') {
loadThirdParty();
} else {
window.addEventListener('load', loadThirdParty);
}
function loadThirdParty() {
const script = document.createElement('script');
script.src = 'https://third-party.com/script.js';
script.async = true;
document.body.appendChild(script);
}
return () => window.removeEventListener('load', loadThirdParty);
}, []);3. Load on Interaction
// Load chat widget only when user interacts
function ChatWidget() {
const [loaded, setLoaded] = useState(false);
const loadChat = () => {
if (loaded) return;
const script = document.createElement('script');
script.src = 'https://chat-widget.com/widget.js';
script.async = true;
document.body.appendChild(script);
setLoaded(true);
};
return (
<button
onClick={loadChat}
onMouseEnter={loadChat} // Preload on hover
>
Open Chat
</button>
);
}4. Load on Visibility
// Load script when element is visible
function LazyThirdParty() {
const ref = useRef<HTMLDivElement>(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !loaded) {
const script = document.createElement('script');
script.src = 'https://ads.com/script.js';
script.async = true;
document.body.appendChild(script);
setLoaded(true);
observer.disconnect();
}
},
{ rootMargin: '200px' } // Load 200px before visible
);
if (ref.current) {
observer.observe(ref.current);
}
return () => observer.disconnect();
}, [loaded]);
return <div ref={ref}>{/* Ad container */}</div>;
}5. Idle Loading
// Load when browser is idle
useEffect(() => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
const script = document.createElement('script');
script.src = 'https://analytics.com/script.js';
script.async = true;
document.body.appendChild(script);
});
} else {
// Fallback for browsers without requestIdleCallback
setTimeout(() => {
const script = document.createElement('script');
script.src = 'https://analytics.com/script.js';
script.async = true;
document.body.appendChild(script);
}, 1000);
}
}, []);Next.js Script Component
import Script from 'next/script';
export default function Page() {
return (
<>
{/* Load before page is interactive (rarely needed) */}
<Script
src="https://critical.com/script.js"
strategy="beforeInteractive"
/>
{/* Load after page is interactive (most scripts) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
strategy="afterInteractive"
onLoad={() => {
// Initialize after load
window.gtag('config', 'GA_ID');
}}
/>
{/* Load when browser is idle (non-critical) */}
<Script
src="https://chat-widget.com/widget.js"
strategy="lazyOnload"
/>
{/* Inline script */}
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
`}
</Script>
</>
);
}Partytown (Web Worker)
Move third-party scripts to a web worker to keep main thread free.
npm install @builder.io/partytown// app/layout.tsx
import { Partytown } from '@builder.io/partytown/react';
export default function RootLayout({ children }) {
return (
<html>
<head>
<Partytown
debug={true}
forward={['dataLayer.push', 'gtag']}
/>
</head>
<body>
{children}
{/* Runs in web worker */}
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_ID" />
<script
type="text/partytown"
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
`,
}}
/>
</body>
</html>
);
}Benefits:
- Main thread stays free
- Better performance
- No blocking
Limitations:
- DOM access limited
- Some scripts may not work
Self-Hosting Third-Party Scripts
# Download and host scripts yourself
curl https://www.googletagmanager.com/gtag/js?id=GA_ID -o public/gtag.js// Use local version
<Script src="/gtag.js" strategy="afterInteractive" />Benefits:
- Control caching
- No third-party DNS/connection overhead
- GDPR compliance
Drawbacks:
- Must update manually
- Lose automatic updates
Facades (Lazy Loading)
Replace third-party embeds with facades that load on click.
YouTube Facade
function YouTubeFacade({ videoId }: { videoId: string }) {
const [loaded, setLoaded] = useState(false);
if (loaded) {
return (
<iframe
src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
style={{ width: '100%', height: '100%' }}
/>
);
}
return (
<div
onClick={() => setLoaded(true)}
style={{
backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`,
backgroundSize: 'cover',
cursor: 'pointer',
position: 'relative',
}}
>
{/* Play button */}
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}>
▶️
</div>
</div>
);
}
// Usage
<YouTubeFacade videoId="dQw4w9WgXcQ" />Savings: 500-700 KB and 500-1000ms until clicked!
React-Lite-YouTube-Embed
npm install react-lite-youtube-embedimport LiteYouTubeEmbed from 'react-lite-youtube-embed';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';
<LiteYouTubeEmbed
id="dQw4w9WgXcQ"
title="Video title"
/>Resource Hints
<!-- DNS prefetch for third-party domains -->
<link rel="dns-prefetch" href="//www.google-analytics.com" />
<link rel="dns-prefetch" href="//www.googletagmanager.com" />
<!-- Preconnect to critical third-parties -->
<link rel="preconnect" href="https://cdn.example.com" />Content Security Policy (CSP)
Control which third-parties can load:
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://www.googletagmanager.com
https://www.google-analytics.com;
connect-src 'self'
https://www.google-analytics.com
https://api.example.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
`.replace(/\s{2,}/g, ' ').trim(),
},
],
},
];
},
};Monitoring Third-Party Impact
Performance Observer
// Monitor third-party impact
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
const resource = entry as PerformanceResourceTiming;
// Check if third-party
if (!resource.name.includes(window.location.origin)) {
console.log('Third-party:', {
url: resource.name,
duration: resource.duration,
transferSize: resource.transferSize,
initiatorType: resource.initiatorType,
});
}
});
});
observer.observe({ entryTypes: ['resource'] });
}Lighthouse
npx lighthouse https://example.com --view
# Check "Reduce JavaScript execution time"
# Third-party scripts listed with impactCommon Third-Parties
Google Analytics
// Load after page interactive
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
`}
</Script>Facebook Pixel
<Script id="facebook-pixel" strategy="afterInteractive">
{`
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', 'PIXEL_ID');
fbq('track', 'PageView');
`}
</Script>Intercom / Chat Widgets
// Load on interaction
function IntercomChat() {
const [loaded, setLoaded] = useState(false);
const loadIntercom = () => {
if (loaded) return;
(window as any).Intercom('boot', {
app_id: 'APP_ID',
});
setLoaded(true);
};
return (
<button onClick={loadIntercom}>
Open Chat
</button>
);
}Best Practices
- Load After Interactive: Use
strategy="afterInteractive" - Async Always: Never block main thread
- Lazy Load: Load on interaction or visibility
- Use Facades: YouTube, maps, social embeds
- Self-Host: When possible
- Web Worker: Use Partytown for heavy scripts
- Resource Hints: DNS prefetch, preconnect
- Monitor: Track third-party impact
- CSP: Restrict what can load
- Audit Regularly: Remove unused scripts
Decision Framework
Is it critical for initial render?
├─ Yes → strategy="beforeInteractive" (rare!)
└─ No
├─ Is it needed immediately after page load?
│ ├─ Yes → strategy="afterInteractive"
│ └─ No
│ ├─ Does user need to interact first?
│ │ ├─ Yes → Load on interaction
│ │ └─ No → strategy="lazyOnload"
│
└─ Can it run in web worker?
├─ Yes → Use Partytown
└─ No → Defer to idleCommon Pitfalls
❌ Loading in <head>: Blocks rendering
✅ Load after page interactive
❌ Sync loading: Blocks parsing
✅ Always async
❌ All third-parties at once: Overwhelms network
✅ Prioritize and stagger
❌ No monitoring: Can't optimize
✅ Track impact in production
❌ Trusting third-parties: They can change
✅ Self-host when possible
Third-party scripts are the #1 performance killer—manage them aggressively and see dramatic improvements!