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.
| Option | Type | Purpose |
|---|---|---|
forward | string[] | Methods to proxy back to the main thread (for example "dataLayer.push" for GTAG). |
debug | boolean | Enables verbose logging in the console to inspect proxied calls. |
lib | string | Path where Partytown serves its library files. Defaults to /~partytown/. |
resolveUrl | function | Rewrites outbound request URLs, useful for proxying through a reverse proxy to dodge ad blockers. |
Tip: The
forwardarray is the most common stumbling block. Analytics libraries push events onto a global queue (likedataLayerorgtag). Because that global lives on the main thread, you must list every entry-point method inforwardor 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
Atomicswith proper COOP/COEP headers). In some sandboxed preview environments service workers are blocked, so always validate on a real deployment, not justlocalhost.
Best practices
- Reserve Partytown for fire-and-forget third-party scripts; keep latency-sensitive, DOM-driving code on the main thread.
- Always populate
forwardwith 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:varsto inject tracking IDs from server-side config instead of duplicating them across files. - Enable
debug: truewhile 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.