You loaded analytics through next/script with strategy="afterInteractive", yet a Performance trace still shows the vendor’s evaluation and event handlers eating 150–300 ms of main-thread time during the interactive window, and your field INP has crept above 200 ms.
Triage: Confirming the Script Runs on the Main Thread
afterInteractive moves when a script runs; it does nothing about where. Confirm the main thread is the bottleneck before changing anything.
-
Record a throttled main-thread trace. Open DevTools → Performance, enable 4× CPU throttling, and record a load plus a couple of interactions. In the Main track, look for tasks attributed to the vendor’s script URL.
-
Attribute the long tasks. Expand any task over 50 ms and read the call stack. If the top frames are the vendor bundle (e.g.
gtm.js,analytics.js) evaluating or handlingpushState/event callbacks, that work is on the main thread and directly inflating Total Blocking Time and INP. -
Confirm the strategy in use. In the React tree or source, verify the
<Script>isafterInteractive(or the default). This tells you the script is deliberately scheduled post-hydration but not offloaded. -
Run the reproduction checklist:
Root Cause: afterInteractive Still Executes on the Main Thread
The afterInteractive strategy schedules the script to inject and run just after hydration, but it runs in the page’s main JavaScript context like any other tag. A heavy vendor bundle parses, evaluates, and then attaches long-lived event and history listeners — all competing with React and with user input for the single main thread. This is the exact cost noted in the guide to isolating scripts with the Next.js Script component: strategy controls timing, not thread.
The fix is to change where the code runs. Next.js ships a built-in integration with Partytown behind strategy="worker", which relocates the third-party script into a web worker. Partytown runs the vendor code inside the worker and proxies its document/window/localStorage access back to the main thread through a service worker using synchronous XHR, so the script believes it is running normally while the main thread stays free. This is Next.js’s opinionated path to the general technique described in offloading heavy scripts to web workers.
Resolution: strategy=“worker” Plus the Partytown Setup
Three pieces are required: the feature flag, the Partytown package, and the strategy change. Miss any one and the script silently falls back to the main thread or fails to load.
1. Enable the experimental worker strategy in next.config.js.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
// Required for strategy="worker" to relocate scripts into Partytown.
nextScriptWorkers: true,
},
};
module.exports = nextConfig;
2. Install Partytown. Next.js does not bundle it; the worker strategy expects it as a peer.
npm install @builder.io/partytown
On next dev or next build, Next.js copies the Partytown library into /_next/static/~partytown/ and registers the service worker that proxies DOM access. No manual <Partytown /> component is needed when you use strategy="worker" — the framework wires it.
3. Switch the script to strategy="worker". Only the strategy value changes; the rest of the component is identical.
// app/analytics.tsx
'use client';
import Script from 'next/script';
export function Analytics() {
return (
<>
{/* This bootstrap runs on the main thread — it only queues commands. */}
<Script id="ga-bootstrap" strategy="worker">
{`window.dataLayer=window.dataLayer||[];
window.gtag=function(){dataLayer.push(arguments);};
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');`}
</Script>
{/* The heavy vendor bundle now evaluates inside the Partytown worker. */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
strategy="worker"
/>
</>
);
}
4. Mind the DOM-access caveats. Partytown proxies document and window, but proxying is synchronous and comparatively slow, and a few APIs do not survive the round trip:
- Scripts that read layout (
getBoundingClientRect,offsetWidth) or mutate the DOM on a hot path will be sluggish or visibly wrong — do not offload widgets, maps, or anything that paints. - Direct DOM event listeners attached in the worker are proxied; latency-sensitive handlers (drag, scroll, input) belong on the main thread, not in Partytown.
- Some SDKs feature-detect by checking for real
windowproperties and refuse to run in a proxied context; test each vendor rather than assuming. - If the vendor needs to reach globals your first-party code sets (e.g. a
dataLayer), you may needdata-partytown-configforwardentries so calls are relayed. Forgtag/GTM the standard forward isdataLayer.push.
Restrict worker to pure measurement scripts — analytics, tag managers firing events, conversion pixels. Everything with a synchronous visual dependency stays on afterInteractive or lazyOnload.
Verification
Record the same throttled Performance trace after the change. The decisive check: the vendor script’s evaluation and event work now appears under a Worker track (or a partytown service-worker task), and the Main track no longer shows a longtask attributed to the vendor URL during the interactive window. Confirm in DevTools → Application → Service Workers that the Partytown worker is activated, and in Network that requests to /_next/static/~partytown/ succeed. In the field, INP should fall back under 200 ms once the main-thread contribution of the offloaded bundle is gone, while analytics events still arrive in the vendor dashboard.
Common Pitfalls
- Offloading a script that touches layout or paints. Partytown’s synchronous DOM proxy is slow; a maps embed, carousel, or chat widget offloaded to the worker will jank or misrender. Keep those on the main thread and offload only measurement code.
- Forgetting the
nextScriptWorkersflag or the package. Withoutexperimental.nextScriptWorkersinnext.config.js,strategy="worker"is ignored; without@builder.io/partytowninstalled, the build cannot copy the library and the script fails to run at all. Both are required together. - Assuming every vendor works off-thread. Some SDKs feature-detect a real
windowand refuse to initialize under Partytown, or depend on cookies written synchronously. Verify each vendor in a trace and dashboard before shipping, and fall back toafterInteractivefor any that misbehave.