Fix Third-Party Script Impact on INP
Third-party scripts consume an average of 30% of main thread time on commercial websites. That's not your code causing INP problems—it's analytics, ads, chat widgets, and embedded content fighting for the same thread your users need for interactions.
What's the Problem?
Every third-party script runs on the main thread. When a user clicks a button, but the browser is busy executing Google Tag Manager callbacks or ad rendering code, that click waits. The HTTP Archive reports median sites load 21 third-party resources totaling 450KB.
Third-party impact on INP happens three ways:
Input delay — User clicks while third-party code is executing. The browser queues the interaction until the main thread is free.
Processing interference — Your event handler runs, but third-party code also runs during the same task, extending total blocking time.
Periodic execution — Many scripts don't just run on load. Analytics libraries send beacons, ad scripts refresh, and chat widgets poll for new messages. These ongoing tasks compete with interactions throughout the session.
Hidden Cost: Structured Data & Meta Tags
We often think of SEO tags as "free," but injecting massive JSON-LD blobs or complex meta tags via JavaScript can block the main thread.
If you use a tag manager or a CMS plugin to inject Schema.org data:
- The browser parses the script (Main Thread)
- The browser serializes the JSON (Main Thread)
- The browser inserts it into the DOM (Main Thread)
For large product catalogs, a 50KB JSON-LD object can cause a 100ms+ blocking task just to "help SEO"—ironically hurting your Core Web Vitals ranking factor.
The Lighthouse audit "Reduce the impact of third-party code" identifies scripts blocking the main thread. It measures transfer size and main thread execution time per third-party origin.
How to Identify the Issue
Open Chrome DevTools Performance panel, record an interaction, and look at the main thread. Third-party code appears as tasks from external domains. Sort the call tree by "Self Time" to find the worst offenders.
For ongoing monitoring, measure third-party main thread time:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
const script = entry.attribution?.[0]?.containerSrc
if (script && !script.includes(location.hostname)) {
console.log('Third-party long task:', script, entry.duration)
}
}
}
})
observer.observe({ entryTypes: ['longtask'] })
The Fix
Load Non-Essential Scripts on Interaction
The facade pattern shows a lightweight placeholder, then loads the full script when users interact. YouTube embeds are the classic example—a thumbnail and play button cost a few KB, while the full player loads 1MB+.
class YouTubeFacade extends HTMLElement {
connectedCallback() {
const videoId = this.getAttribute('video-id')
this.innerHTML = `
<img src="https://i.ytimg.com/vi/${videoId}/hqdefault.jpg"
alt="Video thumbnail" loading="lazy">
<button aria-label="Play video">Play</button>
`
this.querySelector('button').onclick = () => this.loadPlayer()
}
loadPlayer() {
const videoId = this.getAttribute('video-id')
this.innerHTML = `
<iframe src="https://www.youtube.com/embed/${videoId}?autoplay=1"
allow="autoplay; encrypted-media" allowfullscreen></iframe>
`
}
}
customElements.define('youtube-facade', YouTubeFacade)
Apply this pattern to chat widgets, social embeds, maps, and any interactive third-party content below the fold.
Use async or defer Appropriately
Both attributes prevent render blocking, but they execute differently:
async— Downloads in parallel, executes immediately when ready. Order not guaranteed.defer— Downloads in parallel, executes after HTML parsing, in document order.
<!-- Analytics: doesn't need DOM, order doesn't matter -->
<script async src="https://analytics.example.com/track.js"></script>
<!-- Widget that needs DOM: defer ensures HTML is ready -->
<script defer src="https://widget.example.com/embed.js"></script>
For scripts that must execute in order (like GTM loading other scripts), use defer. For independent analytics, use async.
Self-Host Critical Third-Party Code
External scripts require DNS lookup, TCP connection, and TLS handshake—adding 100-500ms before download starts. Self-hosting eliminates this latency.
curl -o public/gtm.js https://www.googletagmanager.com/gtm.js?id=GTM-XXXX
Self-hosted scripts also benefit from your CDN, HTTP/2 multiplexing, and caching strategy. Update periodically to get vendor fixes.
Be aware: some scripts check their origin or rely on same-origin cookies. Test thoroughly before self-hosting.
Use Web Workers for Analytics
Web Workers run JavaScript off the main thread. Tools like Partytown proxy third-party scripts into a worker:
<script>
partytown = {
forward: ['dataLayer.push', 'gtag']
}
</script>
<script src="/~partytown/partytown.js"></script>
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js"></script>
The worker communicates with the main thread through postMessage, but third-party execution no longer blocks interactions. Early adopters report 50-70% TBT reduction.
Caveats: not all scripts work in workers (DOM access requires proxying), and debugging becomes harder. Test critical user flows.
Set Resource Hints
Preconnect warms up connections to third-party origins you know you'll need:
<link rel="preconnect" href="https://www.google-analytics.com">
<link rel="dns-prefetch" href="https://static.ads.example.com">
Use preconnect for critical third parties (2-3 max—more wastes bandwidth). Use dns-prefetch for less critical origins where you want DNS resolved early but don't need the full connection.
Framework-Specific Solutions
next/script component provides loading strategies:import Script from 'next/script'
// Worker strategy uses Partytown
<Script src="https://www.googletagmanager.com/gtag/js" strategy="worker" />
// lazyOnload waits until page is idle
<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />
// afterInteractive loads after hydration (default)
<Script src="https://analytics.example.com/track.js" strategy="afterInteractive" />
useScript composable with triggers:// Load when browser is idle
useScript('https://analytics.example.com/track.js', {
trigger: 'idle'
})
// Load when element is visible
const chatContainer = ref(null)
useScript('https://chat.example.com/widget.js', {
trigger: useScriptTriggerElement({ el: chatContainer })
})
// Load on manual trigger
const { load } = useScript('https://maps.example.com/api.js', {
trigger: 'manual'
})
// Later: load()
@nuxtjs/partytown module.Verify the Fix
After changes, measure the impact:
- Run Lighthouse—check "Third-party code blocked the main thread" diagnostic
- Record a Performance trace during interaction—third-party tasks should be shorter or absent
- Monitor RUM data—compare INP before and after deployment
Expect TBT reduction of 100-500ms when moving analytics to workers or deferring heavy embeds.
Common Mistakes
Loading everything on page load — Even deferred scripts execute eventually. Use facades and lazy loading for content users might never interact with.
Preconnecting to too many origins — Each preconnect consumes bandwidth and CPU. Limit to 2-3 critical origins.
Breaking tracking — Deferred analytics may miss early page events. Ensure critical tracking fires before users can navigate away.
Testing only on fast connections — Third-party impact is far worse on mobile. Test on throttled connections.
Third-Party Triage
Not all third parties are equal. Prioritize by business impact:
| Category | Priority | Strategy |
|---|---|---|
| Consent management | Critical | Load early, minimize size |
| Analytics | High | Defer or worker |
| A/B testing | High | Load early, minimize blocking |
| Ads | Revenue | Lazy-load when in view |
| Chat widgets | Medium | Facade, load on interaction |
| Social embeds | Low | Facade with placeholder |
| Video embeds | Low | Facade with thumbnail |
Consider removing scripts that don't justify their performance cost. Every analytics tool added has a cumulative impact.
Related Issues
Third-party scripts often combine with other INP causes:
- Long-Running JavaScript — Your code + third-party code compete
- Hydration Issues — Scripts loading during hydration extend blocking time
Test Your Entire Site
Third-party impact varies dramatically by page. A minimal homepage may perform well while product pages with multiple embeds suffer. Unlighthouse scans every page and surfaces TBT problems from third-party scripts across your entire site.