Skip to content
Astro as patterns 4 min read

Offloading Scripts with Partytown

Third-party scripts such as analytics, tag managers, and chat widgets are notorious for hogging the browser’s main thread. They run synchronously alongside your application code, blocking rendering, inflating Total Blocking Time, and dragging down your Core Web Vitals. Partytown is a lazy-loaded library that relocates these scripts to a web worker, freeing the main thread for the work that actually matters to your users. Astro ships an official integration that wires this up with almost no configuration, which fits naturally with Astro’s zero-JS-by-default philosophy.

How Partytown works

Partytown intercepts <script> tags marked with type="text/partytown" and runs them inside a web worker instead of the main thread. Because workers cannot touch the DOM or window directly, Partytown proxies those accesses back to the main thread synchronously using a clever combination of service workers (or atomics). The net effect is that heavy, third-party JavaScript executes off the critical path while still appearing to “just work” from the script’s point of view.

This trade-off is ideal for fire-and-forget scripts: analytics beacons, marketing pixels, and A/B testing tools. It is a poor fit for scripts that need fast, frequent DOM access or that drive interactive UI, since the proxying adds latency.

Installing the integration

Add the integration with Astro’s CLI, which installs the package and updates your config automatically.

npx astro add partytown

If you prefer to wire it up by hand, install the package and register it in astro.config.mjs.

// astro.config.mjs
import { defineConfig } from "astro/config";
import partytown from "@astrojs/partytown";

export default defineConfig({
  integrations: [
    partytown({
      // Forward dataLayer methods so Google Analytics queues work
      config: {
        forward: ["dataLayer.push"],
      },
    }),
  ],
});

Marking a script to run in the worker

Once the integration is registered, opt a script in by setting its type attribute to text/partytown. Below is a complete layout component that loads Google Analytics 4 off the main thread.

---
// src/layouts/BaseLayout.astro
const GA_ID = "G-XXXXXXXXXX";
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <slot name="head" />

    <!-- Runs inside the Partytown web worker -->
    <script type="text/partytown" src={`https://www.googletagmanager.com/gtag/js?id=${GA_ID}`}></script>
    <script type="text/partytown" define:vars={{ GA_ID }}>
      window.dataLayer = window.dataLayer || [];
      function gtag() { dataLayer.push(arguments); }
      gtag("js", new Date());
      gtag("config", GA_ID);
    </script>
  </head>
  <body>
    <slot />
  </body>
</html>

The define:vars directive injects server-side values into the inline script, so you never have to hardcode IDs in two places. Note that these scripts add zero blocking JavaScript to the main thread — the page stays interactive while the worker boots in the background.

Configuration options

The config object passed to the integration is forwarded to Partytown at runtime. The most common keys are listed below.

OptionTypePurpose
forwardstring[]Methods to proxy back to the main thread (for example "dataLayer.push" for GTAG).
debugbooleanEnables verbose logging in the console to inspect proxied calls.
libstringPath where Partytown serves its library files. Defaults to /~partytown/.
resolveUrlfunctionRewrites outbound request URLs, useful for proxying through a reverse proxy to dodge ad blockers.

Tip: The forward array is the most common stumbling block. Analytics libraries push events onto a global queue (like dataLayer or gtag). Because that global lives on the main thread, you must list every entry-point method in forward or the events will silently never fire.

Verifying it works

Run the dev server and open the browser console with Partytown’s debug mode on. You should see the script executing inside the worker rather than the window.

npm run dev

Output:

[Partytown] 🎉 Service worker registered
[Partytown] ⚡️ Worker (0.1) Startup 18.40ms
[Partytown] ⚡️ Worker (0.1) Executed: https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX

In the Network tab, the GA script will show up as a request originating from the Partytown sandbox, and your main-thread flame chart in the Performance panel will be noticeably cleaner.

Warning: Partytown requires a service worker (or Atomics with proper COOP/COEP headers). In some sandboxed preview environments service workers are blocked, so always validate on a real deployment, not just localhost.

Best practices

  • Reserve Partytown for fire-and-forget third-party scripts; keep latency-sensitive, DOM-driving code on the main thread.
  • Always populate forward with the global queue methods your analytics tool relies on, or events will be dropped.
  • Test in production-like conditions, since service-worker availability differs from local previews.
  • Use define:vars to inject tracking IDs from server-side config instead of duplicating them across files.
  • Enable debug: true while integrating, then turn it off to avoid shipping noisy console output to users.
  • Measure before and after with Lighthouse — Partytown’s win shows up as reduced Total Blocking Time and a healthier Interaction to Next Paint.
Last updated June 14, 2026
Was this helpful?