A quiet developer workspace with a noisy party of scripts contained in a separate room, a red Maine Coon cat resting nearby.

Partytown: Move the Noisy Guests Off Your Main Thread

The Orange Cat
The Orange Cat

Your application code is probably not the slowest thing on your page. The slowest thing is almost certainly the pile of third-party JavaScript you bolted on for the marketing team: Google Tag Manager, Google Analytics, a Facebook pixel, a TikTok pixel, maybe HubSpot and Hotjar for good measure. Every one of those scripts runs on the main thread — the exact same single thread that handles your rendering, your event handlers, and every tap or click your users make. They compete with you for that thread, and they usually win the ones you wish they'd lose.

Partytown is a tiny, lazy-loaded library that picks up those resource-hungry scripts and moves them into a web worker, off the main thread entirely. The result is a main thread dedicated to your code, better Total Blocking Time, and a Lighthouse report that stops yelling about "the impact of third-party code." It was created by Builder.io and is now maintained under the QwikDev umbrella, the same team behind the Qwik framework.

One quick housekeeping note before we go further: the package was renamed. The old @builder.io/partytown name is now deprecated in favor of @qwik.dev/partytown. Everything in this article uses the current @qwik.dev/partytown package. If you find an older tutorial referencing the @builder.io scope or the partytown.builder.io docs domain, just mentally swap in the new names.

Why This Is Harder Than It Sounds

Moving a script into a web worker sounds easy until you remember a small detail: web workers cannot touch the DOM. There is no window, no document, no document.cookie. And the only way a worker talks to the main thread is asynchronously, via postMessage. Meanwhile, the third-party scripts you want to relocate were written assuming synchronous DOM access — they read document.title or window.location and expect an answer right now, not in a promise.

Partytown bridges that gap with a genuinely clever stack of tricks:

  • JavaScript Proxies give the worker a sandboxed, fake window and document. When worker code reads or writes a DOM property, the Proxy intercepts it and forwards the operation to the real DOM on the main thread.
  • Atomics mode (the fast path) uses Atomics.wait() over a SharedArrayBuffer so the worker blocks until the main thread writes back the result. It is roughly 10x faster at moving data than the fallback, but it needs modern browsers and cross-origin isolation headers (COOP/COEP).
  • Service Worker mode (the broadly compatible fallback) does something delightfully sneaky: the worker fires a synchronous XMLHttpRequest, which genuinely blocks the worker thread (and blocking a worker thread is totally fine — it's not the main thread). A service worker intercepts that request, does the async round-trip to the main thread to perform the real DOM work, and returns the answer as the XHR response. From the worker's point of view, everything looked perfectly synchronous.

The trade-off baked into all of this: DOM operations from the worker are throttled and batched on purpose. That's exactly what keeps your main thread free, but it also means worker scripts run measurably slower. Perfect for fire-and-forget analytics. Bad for latency-sensitive UI work. Keep that distinction in your back pocket — it determines whether Partytown is a hero or a headache for any given script.

What's In the Box

  • Relocates marked third-party scripts to a web worker, freeing the main thread.
  • Sandboxed, synchronous-feeling DOM access from inside the worker.
  • A forward config to expose worker-created globals (like dataLayer.push or gtag) back on the main-thread window.
  • A debug mode with verbose logging and readable, non-minified lib files.
  • Reverse proxying of cross-origin script requests to satisfy CORS.
  • A strictProxyHas option for scripts that probe namespaces with the in operator (looking at you, FullStory).
  • First-party integrations for React, Next.js, Nuxt, Angular, Astro, Shopify Hydrogen, and plain HTML.

Getting It Onto Your Page

Install the current package:

npm install @qwik.dev/partytown
yarn add @qwik.dev/partytown

Partytown needs its small runtime "lib" files served from your own origin, conventionally under /~partytown/. This is the single most common setup mistake: if these files aren't there, Partytown silently does nothing. Copy them at build time with the provided CLI:

{
  "scripts": {
    "partytown": "partytown copylib public/~partytown"
  }
}

There's also a programmatic copyLibFiles() helper if you'd rather wire this into a build script directly.

Throwing the First Party

The opt-in is deliberately explicit. Partytown never auto-hijacks your scripts — you mark the ones you want relocated by giving them a non-standard type:

<script type="text/partytown">
  /* this code now runs in the worker */
</script>

That type="text/partytown" does double duty. Browsers ignore script types they don't recognize, so the main thread never executes it. And the unusual type acts as a selector Partytown uses to find those scripts and run them inside the worker instead.

In React, you drop in the <Partytown /> component, which injects the configuration script into your document head:

import { Partytown } from "@qwik.dev/partytown/react";

export function Head() {
  return (
    <>
      <Partytown debug={true} forward={["dataLayer.push"]} />

      <script
        type="text/partytown"
        dangerouslySetInnerHTML={{
          __html: `
            (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];
              var 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");
          `,
        }}
      />
    </>
  );
}

The forward array is the part people miss. Google Tag Manager's dataLayer now lives inside the worker, not on your main-thread window. So when your own page code calls window.dataLayer.push(...), it would normally hit nothing. Listing "dataLayer.push" in forward tells Partytown to put a stub on the main-thread window that quietly relays those calls into the worker. The same pattern applies to forward={["gtag"]} for gtag.js or forward={["fbq"]} for the Facebook pixel.

The Next.js Express Lane

If you're on Next.js, you may already have Partytown without knowing it — next/script ships an experimental worker strategy that is Partytown under the hood. Turn it on in your config:

// next.config.js
module.exports = {
  experimental: {
    nextScriptWorkers: true,
  },
};

Then any script you want in the worker just gets a strategy:

import Script from "next/script";

export default function Page() {
  return <Script src="https://example.com/analytics.js" strategy="worker" />;
}

Next will prompt you to install @qwik.dev/partytown as a dependency the first time. One important gotcha: the worker strategy is only supported in the Pages Router. In the Next.js 13+ app/ directory it does nothing, and you'll need to wire Partytown manually — inject the config script yourself and use type="text/partytown" on your tags, exactly like the React example above.

Tuning the Sound System

Once you're past "it works," a few advanced knobs separate a smooth integration from a flaky one.

Choosing Atomics vs. Service Worker

By default Partytown uses the service-worker strategy because it works almost everywhere. If you want the roughly-10x-faster Atomics path, you need to opt in and serve your page with cross-origin isolation headers so SharedArrayBuffer is available:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Without those headers (or on older browsers), Atomics quietly falls back to service-worker mode, so you never end up worse off — you just don't get the speed bump. Enabling cross-origin isolation can have ripple effects on other embeds and resources, so test it across your whole page, not just the analytics scripts.

Proxying Cross-Origin Resources and Quieting FullStory

Because the worker fetches third-party resources differently than the browser would, some cross-origin scripts trip over CORS. Partytown lets you reverse-proxy those requests through your own origin. The Facebook pixel's connect.facebook.net is the classic example — you configure a resolver so those fetches route through a same-origin path that relays to the real host.

And if you run FullStory alongside Google Tag Manager, you'll hit a phantom "namespace conflict." FullStory uses the in operator to check whether a namespace exists, and Partytown's proxy doesn't answer that probe the way FullStory expects. The fix is a single config flag:

<Partytown
  forward={["dataLayer.push"]}
  // @ts-expect-error - passed through to the Partytown config
  strictProxyHas={true}
/>

Reading the Room: When Not to Invite Partytown

Partytown is a sharp, targeted tool, and being honest about its edges is the difference between a win and a regret. It is still officially beta — not guaranteed to work in every scenario — so test each integration individually rather than assuming.

The deeper rule comes straight from how it works. Because every DOM access is a throttled inter-thread round trip, Partytown is fantastic for fire-and-forget telemetry and miserable for interactive UI. A few specific landmines:

  • event.preventDefault() does not work. Worker event handlers run asynchronously, so by the time yours fires, the browser has already done the default action.
  • UI-heavy widgets feel sluggish. Chat bubbles, popups, and forms that inject lots of DOM nodes are poor candidates — the throttling shows.
  • Aggressive setInterval DOM polling causes visible layout thrashing.
  • Cross-origin iframe storage (cookies, localStorage, sessionStorage) created by relocated scripts may not persist as expected.

The scripts that thrive in the worker are exactly the ones dragging down your Core Web Vitals: Google Tag Manager, Google Analytics, Facebook and TikTok pixels, Mixpanel, Klaviyo, HubSpot tracking, Amplitude, Hotjar, and friends. The rule of thumb writes itself — telemetry that talks to a server and forgets about it is a great fit; anything that builds a little app inside your page is not.

The Afterparty

Partytown solves a specific, painful, common problem: you have heavy marketing and analytics tags tanking your performance scores, the marketing team won't let you remove them, you don't control their source, and you don't want to stand up server-side tagging infrastructure to fix it. For that exact situation, a drop-in client-side library that quietly exiles those scripts to a web worker — while convincing them nothing has changed — is something close to magic.

Just remember it's magic with rules. Serve the lib files from your origin, use forward for any global your page still needs to call, reach for strictProxyHas and reverse proxies when integrations misbehave, and keep latency-sensitive UI on the main thread where it belongs. Mark the right scripts with type="text/partytown", send the noisy guests down the hall, and let your main thread get back to the only work it should have been doing all along: yours.