Running Third-Party Scripts in a Web Worker with Partytown
Most third-party scripts on a static site — analytics, tag managers, advertising pixels — do not need to run on the main thread. They collect a few values, wait for events and send beacons. But they run there anyway, and their startup work and event listeners compete with the reader's taps and scrolls. Partytown is a small library that runs such scripts inside a web worker and proxies their DOM access back to the main thread, so their execution time no longer blocks interactions.
It is not a free win. Proxying DOM calls is slower than making them directly, some scripts break, and cross-origin scripts need a proxy. This guide sets Partytown up on a static site, shows which scripts are good candidates, and measures the effect on INP. It is part of Third-Party Script Performance on Static Sites.
Prerequisites
- A static site with third-party scripts you have already audited and trimmed — see Auditing Third-Party Scripts With Lighthouse.
- The ability to add a reverse proxy rule on your CDN or host.
- INP field data with script attribution, to confirm the benefit.
How It Works
Partytown's main-thread library is tiny. It finds <script type="text/partytown"> elements, which browsers ignore because of the unknown type, and runs their contents in a web worker. Inside the worker, window and document are proxies. When a script reads document.title or calls document.createElement, the proxy sends a synchronous request to the main thread, which performs the operation and returns the result.
The synchronous bridge uses Atomics.wait when the page is cross-origin isolated, and a service worker otherwise. Cross-origin isolation requires Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers, which can break embeds, so most static sites use the service worker path.
Setting It Up
Astro. Install the official integration, which copies the library into the build and adds the snippet:
npx astro add partytown
// astro.config.mjs
import partytown from '@astrojs/partytown';
export default { integrations: [partytown({ config: { forward: ['dataLayer.push', 'gtag'] } })] };
Hugo, Eleventy and others. Copy the files from @builder.io/partytown/lib into a static folder such as /~partytown/ during the build (npx partytown copylib static/~partytown), and inline the Partytown snippet in the <head> before any partytown scripts.
Then change the type of each script you want to move:
<script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=G-XXXX"></script>
<script type="text/partytown">
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXX');
</script>
The forward option is what lets main-thread code keep calling gtag() or dataLayer.push(); Partytown creates stub functions on the main thread that forward calls into the worker.
Handling Cross-Origin Scripts
A normal <script src> can load from any origin. A worker loads code with fetch, which is subject to CORS, and many tag vendors do not send Access-Control-Allow-Origin. The fix is a reverse proxy on your own domain and Partytown's resolveUrl hook to route requests through it:
partytown({
config: {
forward: ['dataLayer.push'],
resolveUrl(url) {
if (url.hostname === 'connect.facebook.net') {
const proxy = new URL('/proxy/fb/', location.origin);
proxy.pathname += url.pathname.slice(1);
return proxy;
}
return url;
},
},
});
On Cloudflare, a small Worker route or a Transform Rule can serve /proxy/fb/* from the vendor with a CORS header added; on Netlify and Vercel, a rewrite rule does the same. Keep the proxy narrow — only the hosts you need — so it cannot be used as an open relay.
Which Scripts to Move
Test each script individually; compatibility depends on how the script uses the DOM.
After moving a script, confirm it still does its job in the vendor's tool: GA4 DebugView, Meta's Pixel Helper, LinkedIn's Insight Tag status. A script can load without errors in the worker and still send nothing.
Debugging
Partytown has a debug: true option that logs every proxied call with its timing, and logCalls, logGetters and logSetters options for more detail. Common problems:
- Script loads but sends nothing. Usually a CORS failure on a secondary script the tag loads; add it to the proxy.
- Events pushed from the page never arrive. The function name is missing from
forward. - Script throws on
document.currentScriptor layout reads. It needs synchronous main-thread access; keep it out of the worker. - Tags fire twice. The original
<script>withouttype="text/partytown"is still present in another template. - Nothing runs after a deploy. The library files were not copied into the new build, so the worker script returns 404; check the network panel for
partytown-sw.js.
Alternatives Worth Comparing
Partytown is one way to take third-party work off the main thread. Before committing to it, compare it with the options that remove the work entirely:
- Server-side tagging. A server-side GTM container or a vendor's conversion API receives one first-party beacon from the page and forwards events to each vendor from the server. The browser runs one small script instead of several, and no proxying is needed. It costs a small hosted service, but it also improves data quality under ad blockers.
- Edge collection. A Cloudflare Worker or similar edge function can receive a beacon and forward it to GA4's Measurement Protocol, with no vendor script on the page at all.
- Lighter first-party analytics. Self-hosted analytics with a 1–2 KB script often replaces GA4 for content sites that only need page and referrer data.
- Delayed loading. Loading scripts after the page is idle keeps them out of the first interactions, which is where most INP problems appear, and is far simpler to operate.
Partytown fits best when the scripts must stay client-side, cannot be delayed further, and are simple data collectors. When a script has a server-side alternative, that alternative is usually both faster and more robust.
Measured Impact
A 450-page marketing and documentation site built with Astro loaded GA4, a GTM container with six simple tags, and the Meta and LinkedIn pixels. Moving all four to Partytown, with a Cloudflare Worker proxy for the two pixel hosts, reduced third-party main-thread time in the first ten seconds from 410 to 70 milliseconds on a throttled phone. Field INP p75 on mobile fell from 220 to 160 milliseconds. GA4 event counts were within 1% of the previous period; the Meta pixel initially under-reported by 12% until a missing secondary script host was added to the proxy.
Pitfalls & Rollback
- Moving everything at once. Move one script per release so a data gap can be traced.
- Forgetting the proxy. Without it, many scripts fail silently on CORS.
- Relying on it instead of trimming. A script you do not need is cheaper deleted than moved; see Loading Google Tag Manager Without Hurting INP.
- Security headers. A strict Content Security Policy must allow the worker and the proxy path; see Writing a Content Security Policy for a Static Site.
- Rollback: change
type="text/partytown"back to a normal script; nothing else depends on Partytown.
Conclusion
Partytown is a targeted tool: it takes data-collection scripts that have no reason to run on the main thread and moves them to a web worker, cutting the long tasks that delay interactions. Set it up with the official integration or the copied library, add a narrow reverse proxy for vendors without CORS headers, move scripts one at a time and verify each in the vendor's debugger. Keep UI-rendering and consent scripts on the main thread, and trim before you move.
FAQ
What does Partytown do?
It runs scripts marked with type text/partytown inside a web worker instead of on the main thread. When those scripts read or write the DOM, Partytown proxies the calls to the main thread, so most analytics and pixel scripts work unchanged while their execution time moves off the main thread.
Which scripts work well in Partytown?
Scripts that mostly collect data and send it somewhere - Google Analytics, Google Tag Manager with simple tags, Meta and LinkedIn pixels, many heatmap loaders. Scripts that render UI, need synchronous event handling or measure layout precisely tend to break or behave oddly.
Does Partytown need special server configuration?
Partytown's library files must be served from your own origin, and third-party scripts loaded in the worker need CORS headers or a proxy. Many tag vendors do not send CORS headers, so a small reverse proxy on your CDN is usually required.
Is Partytown still maintained?
It is maintained by the Qwik team and used through official integrations for Astro, Nuxt and Next.js, but development is slower than in its early years. Treat it as a useful tool for specific scripts, and test each upgrade, rather than a default for all third-party code.
Related
- Parent: Third-Party Script Performance on Static Sites — every third-party pattern.
- Loading Google Tag Manager Without Hurting INP — the simpler first step.
- Self-Hosting Analytics to Cut Third-Party Requests — removing the script entirely.
- Proxying Third-Party APIs from an Edge Function — building the CORS proxy.
- Measuring INP on Static Sites with Real User Monitoring — confirming the gain.