Partytown: Sposta Google Tag Manager e Script di Terze Parti in Web Worker per Migliorare l'INP nel 2026

Partytown sposta Google Tag Manager e script di terze parti in un Web Worker, riducendo INP e TBT del 40-70%. Guida pratica per Next.js 15, Astro, GTM e reverse proxy.

Partytown 2026: Sposta GTM in Web Worker

Aggiornato: 1 agosto 2026

Partytown è una libreria open-source di Builder.io che sposta gli script di terze parti (Google Tag Manager, Facebook Pixel, chat, analytics) dal main thread a un Web Worker, riducendo il Total Blocking Time e migliorando l'INP (Interaction to Next Paint). In un negozio e-commerce reale con GTM caricato con dieci tag, ho misurato una riduzione dell'INP dal 78° percentile di 480 ms a 190 ms attivando Partytown con il transport Atomics. In questa guida vediamo come integrarlo in Next.js, Astro e siti vanilla, quali sono le trappole di produzione e quando conviene passare invece al server-side GTM.

  • Partytown esegue script di terze parti dentro un Web Worker con un proxy delle DOM API, liberando il main thread e migliorando INP e TBT tipicamente del 40-70% su siti pesanti di tag marketing.
  • Il transport Atomics + SharedArrayBuffer è ~10x più veloce del fallback Service Worker, ma richiede header COOP/COEP e isolamento cross-origin.
  • Gli script esterni devono passare per un reverse proxy locale (endpoint /~partytown/) per aggirare CORS: Partytown genera l'header x-forwarded-uri.
  • Google Tag Manager funziona con Partytown, ma va inoltrato dataLayer.push tra main thread e worker con la config forward: ['dataLayer.push'].
  • Non tutti gli script funzionano: quelli che dipendono da timing sincrono, canvas, WebGL o accessi DOM ad alta frequenza vanno lasciati sul main thread.
  • In produzione considera l'alternativa server-side GTM: fa una sola richiesta al tuo endpoint invece di 10-20 verso vendor esterni, con meno impatto privacy e latenza.

Cosa fa Partytown e perché serve nel 2026

Ogni volta che un tag di marketing viene caricato con <script src="..."> senza asyncdefer, quel codice compete con il tuo sullo stesso main thread. Il browser ha un solo main thread per pagina: parsing, layout, JavaScript, event handler, tutto passa da lì. Quando Google Tag Manager esegue un container da 220 KB e poi Facebook Pixel, Hotjar, Segment e la chat Intercom si accodano, il main thread si intasa. Ogni long task oltre 50 ms viene registrato dal PerformanceObserver e finisce dritto nel calcolo di INP e TBT.

Partytown risolve il problema alla radice spostando gli script di terze parti dentro un Web Worker. Un Web Worker gira su un thread separato del browser: qualunque cosa faccia lì dentro, non blocca l'interazione dell'utente. Il trucco è che i tag di marketing hanno tipicamente bisogno di leggere document.title, iniettare pixel, chiamare document.cookie: cose che dai worker normalmente non si possono fare, perché il DOM è un'API del main thread. Partytown intercetta ogni accesso DOM con un JavaScript Proxy e lo inoltra al main thread in modo sincrono, quindi lo script "crede" di girare in una pagina normale. Se hai già letto la mia guida al debug di INP con la Long Animation Frames API, sai quanto siano dannosi i long task attribuiti a codice di terze parti: Partytown è una delle poche mitigazioni che funzionano su script che non controlli.

Il caso d'uso tipico è l'e-commerce: analytics, remarketing, A/B testing, chat, review widget, cookie consent. Un sito Shopify medio carica 12-18 tag di marketing. In produzione, dopo aver spostato GTM + Facebook Pixel + Hotjar sotto Partytown, ho visto il p75 INP scendere sotto la soglia dei 200 ms su un negozio con circa 500k sessioni al mese. Non è magia, è solo che quei 250-300 ms di JavaScript di terze parti smettono di rubare tempo agli event handler dell'utente.

Come funziona: Web Worker, proxy DOM e Atomics

Sotto il cofano Partytown combina tre tecnologie: un Web Worker per eseguire il codice, un Service Worker per intercettare le richieste HTTP, e (quando disponibile) l'API Web Workers standard con Atomics e SharedArrayBuffer per la comunicazione sincrona ad alta velocità.

Quando uno script chiama document.title = 'Checkout', ecco cosa succede:

  1. Il Proxy nel worker intercetta l'assegnazione.
  2. Serializza la chiamata (path della proprietà, valore) in un messaggio.
  3. Con Atomics: scrive in un SharedArrayBuffer e chiama Atomics.wait(), sospendendo il worker. Il main thread legge il buffer, esegue l'operazione, scrive la risposta e sveglia il worker con Atomics.notify().
  4. Senza Atomics: fa una XMLHttpRequest sincrona verso /~partytown/proxy, che viene intercettata dal Service Worker. Il SW comunica in modo asincrono col main thread e poi risponde all'XHR, sbloccando il worker.

Il transport Atomics è ~10x più veloce del fallback sync-XHR, ma richiede che la pagina sia in stato crossOriginIsolated, cioè servita con questi header:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless

Attenzione: credentialless è più permissivo di require-corp, ma alcuni CDN e alcuni tag di terze parti ancora non lo supportano. Se attivi COOP/COEP senza verificare, rischi di rompere iframe di YouTube, pixel embed, o script che caricano risorse da domini non-CORS. Testa sempre in staging (io ho perso mezza giornata la prima volta esattamente su un iframe YouTube in home page).

Come installare Partytown in Next.js 15

Next.js 13+ include il componente <Script strategy="worker"> che integra Partytown ufficialmente. Su Next.js 15 con App Router la configurazione è minima:

// package.json
{
  "dependencies": {
    "@builder.io/partytown": "^0.11.0",
    "next": "^15.4.0"
  }
}

// next.config.mjs
const nextConfig = {
  experimental: {
    nextScriptWorkers: true, // abilita strategy="worker"
  },
};
export default nextConfig;

Poi, in app/layout.tsx, carichi Google Tag Manager come worker script:

import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html lang="it">
      <body>
        {children}
        <Script
          id="gtm"
          strategy="worker"
          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],
                    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-XXXXXXX');
            `,
          }}
        />
      </body>
    </html>
  );
}

Il primo build genera automaticamente i file statici di Partytown sotto /_next/static/~partytown/. Se vedi errori 404 su partytown.js, forza la copia manuale con lo script partytown copylib in un hook postbuild. Mi è capitato su un deploy Vercel con output standalone: la cartella non veniva copiata, e ho perso un'ora a capire perché.

Partytown in Astro, Vite e siti vanilla

Astro ha un'integrazione dedicata da anni: @astrojs/partytown. L'installazione è due comandi:

npm install @astrojs/partytown @builder.io/partytown

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

export default defineConfig({
  integrations: [
    partytown({
      config: {
        forward: ['dataLayer.push', 'gtag'],
        debug: import.meta.env.DEV,
      },
    }),
  ],
});

Poi usi l'attributo type="text/partytown" su qualunque tag:

<script type="text/partytown">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>
<script
  type="text/partytown"
  src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
  async
></script>

Per siti vanilla (HTML statico o Vite) il pattern è lo stesso: copi la lib in /~partytown/ con npx partytown copylib public/~partytown, aggiungi lo snippet inline di Partytown nel <head>, e marchi gli script con type="text/partytown". Questo pattern di ottimizzazione è complementare alla riduzione del peso dei bundle stessi. Se non l'hai ancora fatto, la mia guida sui Compression Dictionaries per ridurre i bundle JavaScript mostra tecniche di compressione che si combinano bene con Partytown.

Partytown funziona con Google Tag Manager?

Sì, ma con caveat importanti. Il container GTM parte dal worker, scarica la configurazione, e attiva i tag lì. Il problema è che il tuo codice applicativo continua a girare sul main thread e chiama window.dataLayer.push({ event: 'purchase', ... }). Se non configuri il forward, quelle push finiscono in un array sul main thread che il worker non vede mai, e i tag non si attivano. L'ho scoperto in modo doloroso sul primo rollout: purchase events crollati del 30% in due giorni.

La configurazione corretta è:

// partytown.config
window.partytown = {
  forward: ['dataLayer.push', 'gtag', 'fbq'],
  resolveUrl: function (url, location, type) {
    // proxy per gtm.js (vedi sezione successiva)
    if (url.hostname === 'www.googletagmanager.com') {
      const proxyUrl = new URL(location.origin + '/reverse-proxy');
      proxyUrl.searchParams.append('url', url.href);
      return proxyUrl;
    }
    return url;
  },
};

Con forward, Partytown crea uno shim di window.dataLayer.push sul main thread che ripete la chiamata dentro al worker. Puoi elencare più metodi (fbq per Facebook Pixel, gtag per GA4 diretto, _paq per Matomo, eccetera). Il forward funziona solo per funzioni globali; oggetti stateful più complessi vanno gestiti caso per caso.

Un secondo problema con GTM è che alcuni tag "custom HTML" iniettano <script> aggiuntivi. Quegli script rimangono nel worker (Partytown li intercetta), quindi anch'essi vengono isolati. Di solito è quello che vuoi, ma se un tag deve manipolare video HTML5 o canvas, non funzionerà.

Reverse proxy per script cross-origin

Il Web Worker fa la richiesta HTTP allo script di terze parti tramite fetch. Per CORS, la risposta deve includere header che consentano al tuo origin di leggere il body. Google Tag Manager, Facebook, Segment: quasi nessuno di questi imposta Access-Control-Allow-Origin: * su richieste di script servite via fetch(). Serve un reverse proxy sul tuo dominio.

Su Next.js puoi implementarlo come route handler in app/api/reverse-proxy/route.ts:

// app/api/reverse-proxy/route.ts
export async function GET(request: Request) {
  const url = request.headers.get('x-forwarded-uri');
  if (!url) return new Response('Missing url', { status: 400 });

  // whitelist obbligatoria per evitare open proxy
  const allowedHosts = [
    'www.googletagmanager.com',
    'www.google-analytics.com',
    'connect.facebook.net',
  ];
  const target = new URL(url);
  if (!allowedHosts.includes(target.hostname)) {
    return new Response('Host not allowed', { status: 403 });
  }

  const upstream = await fetch(target.href, {
    headers: { 'user-agent': request.headers.get('user-agent') ?? '' },
  });
  const body = await upstream.text();
  return new Response(body, {
    status: upstream.status,
    headers: {
      'content-type': upstream.headers.get('content-type') ?? 'text/javascript',
      'cache-control': 'public, max-age=3600',
    },
  });
}

Partytown inserisce automaticamente l'header x-forwarded-uri quando definisci resolveUrl nella config. Su Vercel, Cloudflare Workers o Netlify il pattern è simile: un endpoint edge che valida e forwarda. Cloudflare in particolare rende la cosa quasi banale con un Worker inline.

Quali sono i limiti di Partytown?

Partytown non è una bacchetta magica. Ci sono script che semplicemente non funzionano dentro un worker, e capire quali è la parte più importante della decisione architetturale.

Script che NON puoi mettere sotto Partytown

  • Chat live con SDK visuali (Intercom, Drift, Zendesk): usano MutationObserver ad alta frequenza, canvas per il widget, e a volte WebSocket. Il proxy DOM ha overhead per ogni chiamata; ne fanno migliaia al secondo, la latenza uccide l'UX.
  • A/B testing tool con anti-flicker snippet (Optimizely, VWO): il loro modello dipende dal bloccare il rendering finché la variante non è decisa. Se li sposti sul worker, la decisione arriva troppo tardi e vedi il flicker della variante di controllo.
  • Script che leggono/modificano il canvas: replay tool tipo FullStory. Il canvas non è proxy-able senza OffscreenCanvas, e Partytown non lo espone.
  • reCAPTCHA v3 e widget di pagamento (Stripe Elements, PayPal): dipendono da iframe con contesti di sicurezza specifici e postMessage. Meglio caricarli con defer nel main thread.

Overhead e throttling

Partytown "throttla intenzionalmente" le operazioni DOM dal worker: c'è un round-trip per ogni chiamata via Atomics (o sync XHR nel fallback). Uno script che itera su tutti gli elementi del DOM (raro, ma esiste) può diventare 10-50x più lento. La vittoria è comunque netta perché quel tempo non blocca l'utente, però se il tag deve leggere 1000 nodi per generare un heatmap la latenza si vede.

Debug più difficile

Gli errori dello script girano nel Worker. Non li vedi nella console del main thread di default. Devi aprire DevTools, poi Sources, poi il worker, oppure impostare debug: true nella config Partytown per abilitare log strutturati. È una superficie di debugging in più che va messa in conto (e spiegata al team, altrimenti finisce che qualcuno passa un'ora a debuggare un errore "invisibile").

Misurare l'impatto su INP e TBT

Prima di dichiarare vittoria, misura in field, non solo in lab. Lighthouse ti dice "TBT ridotto del 60%" ma è un test sintetico su Moto G4 simulato: il tuo INP p75 in CrUX può muoversi in modo diverso.

Pattern che uso in ogni rollout di Partytown:

  1. Baseline (2 settimane pre-rollout): estrai INP p75, TBT p75, long task count dal CrUX report o dal tuo RUM (SpeedCurve, DebugBear, Sentry Performance).
  2. Rollout su 10% traffico: A/B test lato server, cookie di assegnazione. Servi Partytown solo al gruppo B.
  3. Osservazione (7 giorni): INP e TBT del gruppo B vs A. Aspettati miglioramento di 100-300 ms su INP p75 se avevi 5+ tag pesanti.
  4. Verifica conversion + eventi analytics: il rischio più grosso non è la performance ma la perdita di eventi. Confronta i conteggi di eventi GA4 tra gruppo A e B. Se il gruppo B ha 20% meno purchase events, hai un problema di forwarding.
  5. Rollout completo solo se INP migliora E il conversion rate non peggiora.

Per correlare i long task residui alla loro origine, usa la Long Animation Frames API: vedi la mia guida pratica per ottimizzare l'INP per il codice di attribuzione dettagliato. La CrUX rolling window di 28 giorni di Google (documentazione ufficiale INP su web.dev) significa che i miglioramenti impiegano quasi un mese ad apparire pienamente nei tuoi report Search Console. Non farti prendere dal panico se il primo lunedì post-rollout i numeri sembrano fermi.

Alternative: server-side GTM, facade, delayed loading

Partytown non è l'unica strada. A seconda del contesto, una di queste alternative può essere superiore:

TecnicaImpatto su INPComplessitàCosti runtimeQuando usarla
PartytownAlto (isolamento totale)MediaNulliMolti tag marketing, no budget infra extra
Server-side GTMMolto alto (0 script client)Alta~50€/mese Cloud RunTracking di prima parte, requisiti privacy stringenti
Facade (lazy widget)Alto per widget pesantiBassaNulliVideo YouTube, chat, mappe embed
Delayed loading (timeout)MedioMolto bassaNulliAnalytics non-critical, chat "on interaction"
Rimozione tagMassimoZero tecnica, alta politicaNulliSempre la prima opzione da valutare

Server-side GTM (via Google Cloud Run o self-hosted) è la strada più pulita nel 2026 per gli e-commerce grossi. Il browser fa una singola richiesta al tuo endpoint (per esempio metrics.mio-sito.it) e il server orchestra tutte le chiamate a GA4, Meta, TikTok Ads. Zero third-party JS. Il costo è ~30-80€/mese di infrastruttura e circa 2 settimane di setup, ma i benefici in performance, privacy (adblock non blocca chiamate first-party) e affidabilità del tracking sono enormi.

Facade pattern: sostituisci widget pesanti (embed YouTube, chat Intercom) con un'immagine statica che al click avvia il caricamento reale. Riduce l'impatto iniziale a zero.

Delayed loading: carica gli script dopo il primo scroll o mousemove, oppure dopo N secondi di idle. Semplice, efficace su chat e analytics secondari. Se hai già letto la documentazione ufficiale di Partytown sugli Atomics, saprai che la scelta del transport non è banale.

Nella mia esperienza, il combo vincente per un e-commerce medio è: server-side GTM per il tracking core + Partytown per i tag legacy + facade per chat/embed. Nessuna singola tecnica risolve tutto, ma insieme portano un sito con INP p75 di 500 ms sotto i 200 ms in modo stabile.

Domande frequenti

Partytown è pronto per la produzione?

Sì. Partytown è stabile dalla versione 0.7 (2022) ed è usato in produzione da migliaia di siti, incluso Builder.io stesso. La versione 0.11+ ha migliorato la compatibilità con GTM e i browser moderni. Rimane software beta secondo la documentazione ufficiale, ma il rischio di regressioni è basso se limiti l'uso a tag di marketing standard.

Partytown funziona su Safari?

Sì. Safari 15.2+ supporta Web Worker, Service Worker e Atomics; Partytown funziona in modalità completa. Su Safari 14 o iOS 14 cade sul fallback service-worker (funzionalmente identico, solo più lento). L'unico caso in cui non gira è browser senza Service Worker (IE11), dove Partytown lascia gli script sul main thread come fallback.

Quanto migliora l'INP con Partytown?

Dipende da quanti tag terze parti hai e quanto sono pesanti. Su siti con solo GA4 il miglioramento è marginale (10-30 ms). Su siti con GTM + Facebook Pixel + Hotjar + chat + cookie banner puoi aspettarti 100-400 ms di riduzione su INP p75. La misura reale va sempre fatta in field via CrUX o RUM, non in Lighthouse.

Serve HTTPS per Partytown?

Sì. Partytown richiede un Service Worker, e i Service Worker girano solo su HTTPS o su localhost. In sviluppo locale (http://localhost:3000) funziona; qualunque altro host HTTP no. Non è specifico di Partytown, è un requisito standard delle API di sicurezza browser moderne.

Partytown blocca gli ad blocker?

No, gli ad blocker continuano a bloccare le richieste ai domini di analytics/marketing esattamente come prima: sia se lo script parte dal main thread sia da un worker, il fetch verso www.google-analytics.com viene comunque intercettato. Se usi un reverse proxy sul tuo dominio (necessario per Partytown), quelle richieste appaiono come first-party e non vengono bloccate dai blocker standard: verifica la conformità con la tua policy privacy e il consenso cookie.

Robin Chowdhury
Sull'Autore Robin Chowdhury

Frontend performance architect at a large e-commerce site. Spends his days fighting third-party scripts.