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.
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.
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.
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 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.
async or defer AppropriatelyBoth 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.
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.
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.
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.
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.After changes, measure the impact:
Expect TBT reduction of 100-500ms when moving analytics to workers or deferring heavy embeds.
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.
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.
Third-party scripts often combine with other INP causes:
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.