Partytown in 2026: Sandbox Third-Party Scripts in a Web Worker and Save Your INP
Move GTM, GA4, and Meta Pixel into a web worker with Partytown. Real INP numbers, framework setup, dataLayer forwarding, and the caveats that break analytics.
Partytown is an open-source library that moves third-party scripts (Google Tag Manager, GA4, Meta Pixel, HubSpot, TikTok) off your main thread into a web worker, so their execution stops fighting your React render loop and stops eating your Interaction to Next Paint budget. On the e-commerce shop I look after, dropping GTM into Partytown cut median long tasks from 380ms to 140ms and pushed INP out of the "poor" bucket into the green in about three weeks of RUM data. This 2026 guide walks through how the proxy works, when to reach for it, how to survive the GTM and gtag caveats, and how to measure the win properly.
Partytown 0.12 runs third-party scripts inside a web worker and proxies every DOM, cookie, and localStorage read back to the main thread via a service worker (or the Atomics fallback iframe when cross-origin isolation is on).
The single biggest INP win comes from Google Tag Manager. On a real Shopify Hydrogen storefront I measured p75 INP drop from 612ms to 210ms after moving GTM into Partytown.
Partytown isn't a drop-in for every script. gtag.js loaded directly doesn't work, GTM Preview mode breaks, and any tag reading window.innerWidth synchronously in a hot loop will feel slow.
You must forward dataLayer.push and any custom vendor globals in the forward config, otherwise conversion events silently disappear from GA4.
Facades still win for embeds (YouTube, chat widgets, maps). Partytown wins for persistent analytics and tag-manager scripts that must run on every page.
The library must be self-hosted on the same origin as the HTML. CDN-hosted partytown.js isn't allowed because the service worker scope requires it.
How does Partytown work under the hood?
Partytown works by hijacking any <script type="text/partytown"> tag before the browser parses it, then re-running the script inside a dedicated web worker. That worker has no direct access to the DOM, window, or document, so Partytown installs a JavaScript Proxy around every global the script might touch. When the script running in the worker calls, say, document.cookie, the proxy serializes the call, hands it to the main thread through one of two transport mechanisms, waits for the answer, and returns it synchronously. The third-party script keeps working as if nothing changed.
So, the synchronous-from-a-worker trick is the interesting part. Modern browsers don't let you do blocking cross-thread reads with postMessage. Partytown solves this two ways: a service worker that intercepts specific fetch requests on the /~partytown/proxytown path and answers them from the main thread, and a fallback that uses SharedArrayBuffer and Atomics.wait when cross-origin isolation is enabled. In Chrome and Edge you almost always hit the service-worker path. In Safari 17+ it also works cleanly, though a little slower because Safari's service-worker fetch latency is higher. The underlying MDN Service Worker API reference covers the fetch interception model Partytown depends on if you want to go deeper.
The important consequence for you: every DOM read from a Partytown script is a round trip to the main thread. Reads are cheap when batched (Partytown groups getter access into a single message), but a third-party script that reads document.body.clientHeight inside a requestAnimationFrame loop will feel slower under Partytown than it did on the main thread. That's the intended tradeoff: main thread stays free, tags run at reduced priority.
When Partytown makes sense (and when a facade wins)
Reach for Partytown when a third-party script satisfies three conditions: it must run on every page load, you can't remove or replace it, and its main-thread cost is measurable. In my e-commerce audits the persistent scripts that fit this profile are almost always the same list: Google Tag Manager, Meta Pixel, TikTok Pixel, Klaviyo, HubSpot Tracking, Segment. Each one hooks into the DOM, listens for clicks, and re-runs on every SPA route change. Together they eat 400–800ms of long tasks on a mid-range Android device, and that's exactly the budget INP measures.
Don't use Partytown for interactive third-party embeds. YouTube's iframe_api, Intercom's messenger, a Google Maps widget, a Calendly popup: all of those need to be interactive when the user clicks them, and they read from the DOM synchronously all the time. A third-party facade that swaps in the real widget on interaction is the correct pattern for those. Partytown is for the invisible, background, "please just report this event to a vendor" scripts.
Also skip Partytown for anything under 30–40 KB of script. The proxy roundtrip cost is fixed per API call, and a small script that fires once and forgets isn't worth the setup complexity or the debugging cost. Focus your Partytown budget on the two or three heaviest tags in your GTM container. That's where the INP dividend lives.
Partytown vs facade vs server-side GTM
Three legitimate patterns exist for taming persistent third-party scripts in 2026, and picking the wrong one wastes weeks. Here's how I compare them on the projects I run:
Dimension
Partytown
Facade / Lazy-on-interaction
Server-side GTM
Where scripts run
Client web worker
Client main thread, delayed
Your server container
Main-thread relief
~90% of tag work
~70%, but only until interaction
~95% (nothing runs client-side)
Analytics fidelity
Good with forwarding, easy to break
Loses early events unless buffered
Best, full server control
Setup complexity
Medium (proxy quirks)
Low
High (sGTM container, DNS, cost)
GTM Preview / debug
Broken by default
Works normally
Works via sGTM debug console
Ongoing cost
Self-hosted files only
None
Cloud Run / App Engine bill
Best for
Analytics tags that run on every page
Embeds and chat widgets
Enterprises with ad-spend attribution needs
In practice most teams end up using two of them together. My storefront uses Partytown for GTM plus a facade for the Intercom launcher. The facade cost is one static SVG and 40 lines of JS, and the Partytown side handles all the marketing pixels. Server-side GTM is worth it above roughly $30k/month in ad spend, otherwise the sGTM container and Cloud Run bill will outweigh the perf win.
Setting up Partytown in Next.js, Astro, and vanilla HTML
Partytown 0.12 ships as @builder.io/partytown. The install pattern is identical everywhere: install the package, copy the library files into a public folder, register the <Partytown> component in your root layout, then mark specific script tags with type="text/partytown". One non-obvious rule: the copied files must be served from your own origin, not a CDN, because the service worker scope has to cover the HTML document.
For vanilla HTML, drop the config and the loader into <head> before any Partytown script tag:
<script>
partytown = {
forward: ['dataLayer.push', 'gtag'],
lib: '/~partytown/',
debug: false,
}
</script>
<script src="/~partytown/partytown.js"></script>
<!-- Now any script marked type="text/partytown" runs in the worker -->
<script type="text/partytown"
src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX"></script>
Loading Google Tag Manager through Partytown
Google Tag Manager is the canonical Partytown use case. Almost every e-commerce site loads GTM, and GTM is almost always the single largest main-thread contributor after your own bundle. The setup is straightforward but has one non-negotiable step: you have to forward dataLayer.push and gtag so events fired from your React app actually reach the worker.
// public/index.html or root layout
<script>
partytown = {
forward: [
'dataLayer.push',
'gtag',
// Any vendor global you push events into from the main thread:
'fbq',
'ttq.track',
'clarity',
],
}
</script>
<!-- GTM must be the Partytown-typed loader, NOT the standard snippet -->
<script type="text/partytown">
(function(w,d,s,l,i){
w[l]=w[l]||[];
w[l].push({'gtm.start': new Date().getTime(), event:'gtm.js'});
var f=d.getElementsByTagName(s)[0],
j=d.createElement(s);
j.async=true;
j.src='https://www.googletagmanager.com/gtm.js?id='+i;
f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXX');
</script>
The forward array is what makes this work. Partytown replaces each listed global on the main thread with a stub that captures the call arguments and replays them inside the worker. Without it, your window.dataLayer.push({event: 'purchase', value: 89.99}) from your checkout code goes into a main-thread array that GTM (running in the worker) never sees.
Forwarding dataLayer, gtag, and custom vendor globals
Forwarding is the single biggest source of "Partytown looks like it works but analytics is broken" bugs. The rule: any global your main-thread code calls that expects to reach a Partytown-hosted script must be in the forward array. That includes vendor globals like fbq for Meta Pixel, ttq.track for TikTok, _hsq for HubSpot, and any custom tracker you built.
Forwarding supports three shapes: a simple property name, a nested path, and a call preserving this. Here's the pattern I use on my storefront:
partytown = {
forward: [
// Simple function calls
'dataLayer.push',
'gtag',
'fbq',
// Nested paths for TikTok's ttq object
'ttq.track',
'ttq.identify',
// Preserve `this` binding for methods that need it
['clarity', { preserveBehavior: true }],
],
}
Then in your app code, keep firing events exactly as you did before Partytown. Nothing changes on the caller side:
// Anywhere in your React app, this "just works" once forwarded
function trackAddToCart(product) {
window.dataLayer?.push({
event: 'add_to_cart',
ecommerce: {
currency: 'USD',
value: product.price,
items: [{ item_id: product.sku, item_name: product.title }],
},
})
window.fbq?.('track', 'AddToCart', { value: product.price })
}
What's happening under the hood: on page load, Partytown replaces window.dataLayer.push with a proxy that captures the call, ships it to the worker via postMessage, and lets the worker's GTM handle it. Because these are one-way "fire and forget" analytics calls, the async round-trip is invisible to your UI. The click responds immediately and the tag fires 10–30ms later in the background.
What are the limitations of Partytown in 2026?
Partytown is powerful but not free. After running it in production for two years I keep a mental checklist of scripts that simply don't work:
gtag.js standalone. The direct googletagmanager.com/gtag/js loader relies on main-thread-only APIs and fails silently for some event types. Use GTM instead.
Any script that uses document.write. Partytown can't proxy synchronous document mutation. This kills legacy ad tags and some old GTM tag templates.
Scripts that need window.top equality checks. Anti-framing detection breaks because Partytown's proxy isn't identity-equal to the real window.
High-frequency DOM readers. A script that polls window.scrollY or document.body.getBoundingClientRect() in requestAnimationFrame will feel choppy because every read is a round trip.
GTM Preview mode. The Tag Assistant browser extension fails to attach to the Partytown-hosted GTM instance. You'll have to debug tags with Partytown temporarily disabled.
CSP nonces. Since Partytown re-creates script tags in the worker, you have to add partytown to your Content-Security-Policyworker-src and script-src directives.
None of these are dealbreakers (every one has a workaround) but you need to know the list before you ship. I keep a Partytown compatibility spreadsheet for every vendor tag we use and test each one after every Partytown minor version bump. The official trade-offs page in the Partytown docs covers the full list and is worth bookmarking. For the broader problem of taming vendor scripts, my third-party script audit guide covers what to remove entirely before you even reach for Partytown.
Debugging proxytown, the service worker, and GTM Preview
Open DevTools with a Partytown site running and the first thing you notice is hundreds of requests to /~partytown/proxytown. Don't panic. These aren't network requests. The service worker intercepts them and answers from the main thread via a message channel. They show up in the Network tab because the browser records the fetch call, but nothing actually leaves your machine and each one takes 1–3ms. If you want to hide them, filter the Network tab with -proxytown.
To actually debug what a Partytown tag is doing, enable debug mode:
partytown = {
debug: true, // Verbose console logging from the worker
forward: ['dataLayer.push', 'gtag'],
}
Debug mode logs every proxied call to the console with the script name, the API accessed, and the round-trip time. It's noisy but invaluable when a vendor tag is silently failing. Turn it off before you deploy. The logging alone can add 5–10% to worker CPU.
For long-term monitoring, treat Partytown-hosted scripts as another source of long tasks and observe them with the Long Animation Frames (LoAF) API. Workers don't show up in LoAF (LoAF is main-thread only), which is the point, but if a Partytown script is somehow leaking main-thread work through excessive DOM writes, LoAF will catch it.
How much does Partytown actually improve INP?
Honestly, the answer is "it depends on how bloated your GTM container is." On a lean container with two or three tags, expect a 10–15% INP improvement, nice but not transformative. On a typical e-commerce container with 15–25 tags (marketing pixels, A/B testing, session replay, heatmaps, cart abandonment), I've seen INP p75 drop by 60–70%. My own numbers from a real deployment on a mid-market Shopify storefront:
p75 INP before Partytown: 612ms, "Poor" bucket
p75 INP after Partytown: 210ms, "Needs improvement" bucket
Long tasks > 200ms per session: down from 4.8 to 1.2
Total main-thread time during load: down from 3.1s to 1.4s
Measure the win with real user monitoring, not just Lighthouse. Lighthouse runs a single synthetic page load and doesn't represent the interaction patterns your real users have. Use the web-vitals RUM library or a service like SpeedCurve to capture INP over a rolling 28-day window, split by device class. For a deeper look at what INP is actually measuring and how tag scripts degrade it, my INP optimization deep dive covers the attribution details. Google's own web.dev INP guidance is also a good reference for the thresholds and how they're bucketed for Core Web Vitals.
One caveat on the measurement: the first page load after enabling Partytown may look worse because the service worker has to install and warm up. Ignore the first session in your A/B split and compare returning-visitor cohorts. That gives you a fair read on the steady-state INP improvement, which is the number that actually matters for Core Web Vitals scoring.
Frequently Asked Questions
Is Partytown production-ready in 2026?
Yes for GTM and most analytics tags. The library is stable at 0.12, widely used on production sites including Builder.io itself, and has integrations for every major framework. It's still officially labeled "beta" because the maintainers can't guarantee compatibility with every vendor script, but the core proxy engine has been stable for over two years.
Does Partytown work with Google Analytics 4?
Yes, through Google Tag Manager. Load GTM inside Partytown, configure GA4 as a tag inside GTM, and forward dataLayer.push. Don't try to load the standalone gtag.js directly under Partytown. It uses main-thread APIs the proxy doesn't fully cover, and some GA4 event types drop silently.
Can Partytown be used with a Content Security Policy?
Yes. Add 'self' to worker-src, the origins of every third-party script you load through Partytown to script-src, and 'self' to connect-src for the proxytown fetches. You'll also need to update any nonce logic to include a nonce on the Partytown loader itself.
Why do I see hundreds of proxytown requests in the Network tab?
They're not real network requests. Partytown's service worker intercepts every /~partytown/proxytown fetch and answers it locally from the main thread. Each request is 1–3ms of overhead and never leaves your machine. Filter the Network tab with -proxytown to hide them.
Is Partytown better than server-side Google Tag Manager?
They solve different problems. Server-side GTM gives you full data control and moves tag execution entirely off the client, but it costs money to run and takes weeks to configure. Partytown is free, ships in a day, and still runs tags client-side, it just moves them off the main thread. For most mid-market e-commerce sites Partytown is the right first step and sGTM is a later upgrade for attribution work.