Tech Verse Logo
Enable dark mode
Fix Next.js Third Party Script Performance

Fix Next.js Third Party Script Performance

Md. Mostafijur RahmanMMd. Mostafijur Rahman

Md. Mostafijur Rahman

4 min read

The Real Cost of Analytics and Ad Tags

Adding Google Tag Manager, Hotjar, or Meta Pixel feels harmless until you run a Lighthouse audit. A single container script pulled from an external CDN downloads 50KB of compressed JS, expands to 220KB in memory, and triggers five secondary network requests. Before you know it, your Total Blocking Time (TBT) sits at 480ms and your Interaction to Next Paint (INP) spikes above 300ms on mobile devices.

In Next.js 16 running on React 19, the core rendering engine is fast. If your server renders HTML in 40ms, but your page hangs for half a second while GTM parses third-party tracking pixels, your users don't care about your server response times. They care that clicking a menu button feels sluggish.

Understanding Next.js Script Strategies

The built-in next/script component provides four distinct execution strategies: beforeInteractive, afterInteractive, lazyOnload, and worker. Selecting the wrong one breaks page rendering or drops analytics data.

  • beforeInteractive: Loads before page hydration and main document code executes. Reserved for consent managers (like OneTrust) or essential polyfills. Overusing this hurts First Contentful Paint (FCP).
  • afterInteractive: The default strategy. Loads immediately after the page becomes interactive. Good for high-priority analytics where you can't afford to miss bounce traffic.
  • lazyOnload: Delays execution until browser idle time via requestIdleCallback. Best for chat widgets, social embeds, and secondary telemetry tags.
  • worker: Offloads script execution completely off the main thread into a Web Worker using Partytown. Perfect for heavy analytics scripts that execute constant background tasks.

Implementing Next.js 16 Script Execution

Here is how to structure scripts inside the Next.js 16 App Router. We place runtime tags inside app/layout.tsx to prevent re-executing scripts on client-side route transitions.

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <head>
        {/* Critical Cookie Consent: executes before hydration */}
        <Script
          src="https://cdn.cookielaw.org/scripttemplates/otSDKStub.js"
          strategy="beforeInteractive"
          data-domain-script="12345-abcde"
        />
      </head>
      <body>
        {children}

        {/* Standard Analytics: runs right after page becomes interactive */}
        <Script
          id="gtm-script"
          strategy="afterInteractive"
          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),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
              'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
              })(window,document,'script','dataLayer','GTM-XXXXXXX');
            `,
          }}
        />

        {/* Non-critical support widget: waits until idle */}
        <Script
          src="https://embed.typeform.com/embed.js"
          strategy="lazyOnload"
        />
      </body>
    </html>
  );
}

Offloading Heavy Scripts to Web Workers

When you have dozens of tags firing inside GTM, switching strategy from afterInteractive to worker moves JS parsing off the main UI thread. Next.js relies on Partytown under the hood for this feature.

To enable worker scripts in Next.js 16, install Partytown and enable the experimental flag in next.config.js:

npm install @builder.io/partytown
// next.config.js
/** @type {import('next').NextPage} */
const nextConfig = {
  experimental: {
    nextScriptWorkers: true,
  },
};

module.exports = nextConfig;

Once configured, set strategy="worker" on your script tag:

<Script
  src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX"
  strategy="worker"
/>

Gotchas and Common Traps with Web Workers

Worker execution sounds like magic, but it comes with strict technical limitations. If you don't account for these, your tracking breaks silently.

1. Same-Origin Policy and CORS

Partytown intercepts network requests and DOM access using synchronous XMLHttpRequest calls inside web workers. Browsers block third-party scripts loaded in web workers unless the remote server returns proper CORS headers (Access-Control-Allow-Origin: *).

Because CDNs like Facebook Pixel don't send permissive CORS headers for script tags, you must proxy third-party scripts through your backend server. If you run a Laravel 12 API alongside Next.js 16, set up an endpoint in PHP 8.3 to proxy third-party JS assets.

// routes/api.php in Laravel 12
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Route;

Route::get('/proxy/gtm.js', function () {
    $response = Http::get('https://www.googletagmanager.com/gtm.js', [
        'id' => request('id'),
    ]);

    return response($response->body(), 200)
        ->header('Content-Type', 'application/javascript')
        ->header('Access-Control-Allow-Origin', '*')
        ->header('Cache-Control', 'public, max-age=3600');
});

2. DOM Manipulation Restrictions

Web workers do not have direct access to document or window. Partytown proxies DOM operations over atomic operations using Atomics.wait and SharedArrayBuffer. If a third-party script tries to continuously read element bounding client rects (like a heatmapping tool like Hotjar or Clarity), the synchronous bridge causes massive CPU thrashing, completely defeating the performance gain.

Keep heatmapping, AB testing tools, and scripts that modify visual layout on the main thread using lazyOnload or afterInteractive. Keep pure data-collection pixels (Google Tag Manager, Segment, Mixpanel) in the worker thread.

Using Built-in Third-Party Libraries

Instead of manually writing boilerplate next/script tags for common services, Next.js provides the @next/third-parties package. It wraps Google Tag Manager, Google Analytics, YouTube embeds, and Google Maps in optimized components.

npm install @next/third-parties@latest

Here is how to use it in React 19 / Next.js 16:

// app/layout.tsx
import { GoogleTagManager, GoogleAnalytics } from '@next/third-parties/google';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
      </body>
      <GoogleTagManager gtmId="GTM-XXXXXXX" />
      <GoogleAnalytics gaId="G-XXXXXXX" />
    </html>
  );
}

The GoogleTagManager component automatically defers script evaluation until the main thread finishes initial rendering work, lowering TBT by approximately 180ms compared to a raw <script> tag in the HTML head.

Measuring Real Performance Gains

On a production E-commerce application running React 19 and Next.js 16, we audited page load times before and after restructuring our script loading strategies. The site ran Google Tag Manager, Facebook Pixel, TikTok Pixel, Hotjar, and LiveChat.

  • Naive HTML Head Tags: TBT was 520ms, LCP was 2.8s, INP was 340ms on a mid-range Android phone.
  • Next.js script afterInteractive default: TBT dropped to 310ms, LCP improved to 2.1s, INP dropped to 210ms.
  • Hybrid Strategy (GTM in Web Worker, LiveChat in lazyOnload, Hotjar afterInteractive): TBT dropped to 42ms, LCP hit 1.4s, and INP reached 65ms (Green band).

Don't dump every marketing tag into the document head. Categorize your scripts by urgency, use lazyOnload whenever instant tracking isn't mandatory, and offload pure analytics pixels into web workers using a custom proxy endpoint.

Md. Mostafijur RahmanMMd. Mostafijur Rahman

WRITTEN BY

Md. Mostafijur Rahman

    Latest Posts

    View All

    Fixing Next.js Hydration Mismatch Errors in React 19

    Fixing Next.js Hydration Mismatch Errors in React 19

    Generating Open Graph Images at the Edge in Next.js 16

    Generating Open Graph Images at the Edge in Next.js 16

    Fix Next.js Third Party Script Performance

    Fix Next.js Third Party Script Performance

    Next.js 16 Testing Strategy: Unit, Component, E2E

    Next.js 16 Testing Strategy: Unit, Component, E2E

    Shrinking Next.js Bundle Size: Analyzer & Barrel Fixes

    Shrinking Next.js Bundle Size: Analyzer & Barrel Fixes

    Next.js 16 App Router Auth Patterns with Laravel 12

    Next.js 16 App Router Auth Patterns with Laravel 12

    Next.js i18n Routing: Locales, Hreflang, and Metadata

    Next.js i18n Routing: Locales, Hreflang, and Metadata

    Next.js 16 Caching Architecture: The Four Layers

    Next.js 16 Caching Architecture: The Four Layers

    Dynamic Imports and Code Splitting in Next.js 16

    Dynamic Imports and Code Splitting in Next.js 16

    Next.js Image Optimization: Sizes and Remote Patterns

    Next.js Image Optimization: Sizes and Remote Patterns