Managing Third-Party Scripts: Optimization Strategies
Third-party scripts can destroy performance. Strategic loading prevents them from blocking your site.
Quick Navigation: Defer Non-Critical Scripts ⢠Load on Interaction ⢠Facade Pattern ⢠Self-Hosting ⢠Best Practices
Quick Decision Guide
Optimization Strategies:
Defer loading: Use defer or async attributes for non-critical scripts.
Load on interaction: Chat widgets, feedback forms - load when user shows intent.
Facades: Replace heavy embeds (YouTube, maps) with lightweight placeholders, load real widget on click.
Self-host: Host libraries locally for better caching and control (Google Fonts, analytics bundles).
Result: 2-3 second improvement in Time to Interactive by deferring third-party scripts.
Defer Non-Critical Scripts
Analytics Scripts
<!-- ā Bad: Blocks parsing -->
<script src="https://www.google-analytics.com/analytics.js"></script>
<!-- ā
Good: Deferred -->
<script defer src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<!-- ā
Better: Load on idle -->
<script>
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
const script = document.createElement('script');
script.src = 'https://www.googletagmanager.com/gtag/js?id=GA_ID';
script.async = true;
document.head.appendChild(script);
});
}
</script>Tag Managers
<!-- Load GTM after page interactive -->
<script>
window.addEventListener('load', () => {
(function(w,d,s,l,i){
// GTM code
})(window,document,'script','dataLayer','GTM-XXXX');
});
</script>Load on Interaction
Chat Widgets
// Load Intercom only when user interacts
let chatLoaded = false;
function loadChat() {
if (chatLoaded) return;
chatLoaded = true;
const script = document.createElement('script');
script.src = 'https://widget.intercom.io/widget/APP_ID';
script.async = true;
document.body.appendChild(script);
}
// Load on first interaction
['mousemove', 'scroll', 'keydown', 'click', 'touchstart'].forEach(event => {
document.addEventListener(event, loadChat, { once: true, passive: true });
});
// Or load after delay
setTimeout(loadChat, 5000); // Load after 5 secondsCustomer Support Tools
function SupportButton() {
const [loaded, setLoaded] = useState(false);
const loadWidget = () => {
if (!loaded) {
const script = document.createElement('script');
script.src = 'https://support-widget.com/widget.js';
script.async = true;
script.onload = () => setLoaded(true);
document.body.appendChild(script);
}
};
return (
<button onClick={loadWidget}>
{loaded ? 'Chat with us' : 'Load Chat'}
</button>
);
}Facade Pattern
YouTube Embed Facade
function YouTubeFacade({ videoId }) {
const [loaded, setLoaded] = useState(false);
if (loaded) {
return (
<iframe
src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
allow="autoplay; encrypted-media"
allowFullScreen
/>
);
}
return (
<div
className="youtube-facade"
onClick={() => setLoaded(true)}
style={{
backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`,
cursor: 'pointer'
}}
>
<button className="play-button">ā¶ļø Play Video</button>
</div>
);
}
// Saves ~600KB per video until user clicksGoogle Maps Facade
function MapFacade({ lat, lng }) {
const [showMap, setShowMap] = useState(false);
if (showMap) {
return (
<iframe
src={`https://www.google.com/maps/embed?pb=!1m14!...`}
allowFullScreen
/>
);
}
return (
<div
className="map-placeholder"
onClick={() => setShowMap(true)}
>
<img
src={`https://maps.googleapis.com/maps/api/staticmap?center=${lat},${lng}`}
alt="Map"
/>
<button>Load Interactive Map</button>
</div>
);
}
// Saves ~400KB until user interactsSelf-Hosting
Google Fonts
<!-- ā External: Multiple requests, CORS -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700" rel="stylesheet">
<!-- ā
Self-hosted: Better caching, privacy -->
<style>
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url('/fonts/inter-regular.woff2') format('woff2');
font-display: swap;
}
</style>Analytics Libraries
// Self-host Google Analytics for better control
// Download gtag.js and host it yourself
<script async src="/js/gtag.js"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
</script>Best Practices
Performance Budget
Set limits for third-party code:
Total third-party budget: 200KB
- Analytics: 50KB
- Chat widget: 80KB (loaded on interaction)
- A/B testing: 40KB
- Fonts: 30KB (self-hosted)Audit Third-Party Impact
// Measure third-party impact
const thirdPartyScripts = document.querySelectorAll('script[src^="http"]');
performance.getEntriesByType('resource')
.filter(r => r.initiatorType === 'script')
.forEach(r => {
console.log(`${r.name}: ${r.duration}ms (${(r.transferSize / 1024).toFixed(2)}KB)`);
});Strategies Summary
ā Defer: Analytics, tag managers
ā Async: Non-critical widgets
ā On Interaction: Chat, feedback forms
ā Facade: YouTube, maps, social embeds
ā Self-host: Fonts, common libraries
ā Remove: Unused scripts
ā Never: Synchronous blocking third-party scripts in <head>