Front-end Engineering Lab

Third-Party Scripts

Manage third-party scripts efficiently to prevent performance degradation

Third-Party Scripts

Third-party scripts (analytics, ads, chat widgets) are often the biggest performance bottleneck. They can add seconds to page load time if not managed properly.

The Problem

Third-party scripts typically:

  • Block rendering
  • Execute synchronously
  • Load additional resources
  • Run on main thread
  • Impact Core Web Vitals
Example impact:
Google Analytics:     45 KB, 200ms
Facebook Pixel:       80 KB, 300ms
Chat Widget:          120 KB, 400ms
Ad Scripts:           200 KB, 500ms

Total:                445 KB, 1400ms added to page load!

Loading Strategies

1. Async Loading

<!-- ❌ BAD: Blocks parsing -->
<script src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>

<!-- ✅ GOOD: Async, doesn't block -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>

2. Defer to After Page Load

// Load after page is interactive
useEffect(() => {
  // Wait for page load
  if (document.readyState === 'complete') {
    loadThirdParty();
  } else {
    window.addEventListener('load', loadThirdParty);
  }
  
  function loadThirdParty() {
    const script = document.createElement('script');
    script.src = 'https://third-party.com/script.js';
    script.async = true;
    document.body.appendChild(script);
  }
  
  return () => window.removeEventListener('load', loadThirdParty);
}, []);

3. Load on Interaction

// Load chat widget only when user interacts
function ChatWidget() {
  const [loaded, setLoaded] = useState(false);

  const loadChat = () => {
    if (loaded) return;
    
    const script = document.createElement('script');
    script.src = 'https://chat-widget.com/widget.js';
    script.async = true;
    document.body.appendChild(script);
    setLoaded(true);
  };

  return (
    <button
      onClick={loadChat}
      onMouseEnter={loadChat}  // Preload on hover
    >
      Open Chat
    </button>
  );
}

4. Load on Visibility

// Load script when element is visible
function LazyThirdParty() {
  const ref = useRef<HTMLDivElement>(null);
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting && !loaded) {
          const script = document.createElement('script');
          script.src = 'https://ads.com/script.js';
          script.async = true;
          document.body.appendChild(script);
          setLoaded(true);
          observer.disconnect();
        }
      },
      { rootMargin: '200px' }  // Load 200px before visible
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => observer.disconnect();
  }, [loaded]);

  return <div ref={ref}>{/* Ad container */}</div>;
}

5. Idle Loading

// Load when browser is idle
useEffect(() => {
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      const script = document.createElement('script');
      script.src = 'https://analytics.com/script.js';
      script.async = true;
      document.body.appendChild(script);
    });
  } else {
    // Fallback for browsers without requestIdleCallback
    setTimeout(() => {
      const script = document.createElement('script');
      script.src = 'https://analytics.com/script.js';
      script.async = true;
      document.body.appendChild(script);
    }, 1000);
  }
}, []);

Next.js Script Component

import Script from 'next/script';

export default function Page() {
  return (
    <>
      {/* Load before page is interactive (rarely needed) */}
      <Script
        src="https://critical.com/script.js"
        strategy="beforeInteractive"
      />
      
      {/* Load after page is interactive (most scripts) */}
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
        strategy="afterInteractive"
        onLoad={() => {
          // Initialize after load
          window.gtag('config', 'GA_ID');
        }}
      />
      
      {/* Load when browser is idle (non-critical) */}
      <Script
        src="https://chat-widget.com/widget.js"
        strategy="lazyOnload"
      />
      
      {/* Inline script */}
      <Script id="google-analytics" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'GA_ID');
        `}
      </Script>
    </>
  );
}

Partytown (Web Worker)

Move third-party scripts to a web worker to keep main thread free.

npm install @builder.io/partytown
// app/layout.tsx
import { Partytown } from '@builder.io/partytown/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        <Partytown
          debug={true}
          forward={['dataLayer.push', 'gtag']}
        />
      </head>
      <body>
        {children}
        
        {/* Runs in web worker */}
        <script type="text/partytown" src="https://www.googletagmanager.com/gtag/js?id=GA_ID" />
        <script
          type="text/partytown"
          dangerouslySetInnerHTML={{
            __html: `
              window.dataLayer = window.dataLayer || [];
              function gtag(){dataLayer.push(arguments);}
              gtag('js', new Date());
              gtag('config', 'GA_ID');
            `,
          }}
        />
      </body>
    </html>
  );
}

Benefits:

  • Main thread stays free
  • Better performance
  • No blocking

Limitations:

  • DOM access limited
  • Some scripts may not work

Self-Hosting Third-Party Scripts

# Download and host scripts yourself
curl https://www.googletagmanager.com/gtag/js?id=GA_ID -o public/gtag.js
// Use local version
<Script src="/gtag.js" strategy="afterInteractive" />

Benefits:

  • Control caching
  • No third-party DNS/connection overhead
  • GDPR compliance

Drawbacks:

  • Must update manually
  • Lose automatic updates

Facades (Lazy Loading)

Replace third-party embeds with facades that load on click.

YouTube Facade

function YouTubeFacade({ videoId }: { videoId: string }) {
  const [loaded, setLoaded] = useState(false);

  if (loaded) {
    return (
      <iframe
        src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
        allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
        style={{ width: '100%', height: '100%' }}
      />
    );
  }

  return (
    <div
      onClick={() => setLoaded(true)}
      style={{
        backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg)`,
        backgroundSize: 'cover',
        cursor: 'pointer',
        position: 'relative',
      }}
    >
      {/* Play button */}
      <div style={{ 
        position: 'absolute', 
        top: '50%', 
        left: '50%', 
        transform: 'translate(-50%, -50%)',
      }}>
        ▶️
      </div>
    </div>
  );
}

// Usage
<YouTubeFacade videoId="dQw4w9WgXcQ" />

Savings: 500-700 KB and 500-1000ms until clicked!

React-Lite-YouTube-Embed

npm install react-lite-youtube-embed
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import 'react-lite-youtube-embed/dist/LiteYouTubeEmbed.css';

<LiteYouTubeEmbed
  id="dQw4w9WgXcQ"
  title="Video title"
/>

Resource Hints

<!-- DNS prefetch for third-party domains -->
<link rel="dns-prefetch" href="//www.google-analytics.com" />
<link rel="dns-prefetch" href="//www.googletagmanager.com" />

<!-- Preconnect to critical third-parties -->
<link rel="preconnect" href="https://cdn.example.com" />

Content Security Policy (CSP)

Control which third-parties can load:

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: `
              default-src 'self';
              script-src 'self' 'unsafe-inline' 'unsafe-eval' 
                https://www.googletagmanager.com 
                https://www.google-analytics.com;
              connect-src 'self' 
                https://www.google-analytics.com 
                https://api.example.com;
              img-src 'self' data: https:;
              style-src 'self' 'unsafe-inline';
            `.replace(/\s{2,}/g, ' ').trim(),
          },
        ],
      },
    ];
  },
};

Monitoring Third-Party Impact

Performance Observer

// Monitor third-party impact
if ('PerformanceObserver' in window) {
  const observer = new PerformanceObserver((list) => {
    list.getEntries().forEach((entry) => {
      const resource = entry as PerformanceResourceTiming;
      
      // Check if third-party
      if (!resource.name.includes(window.location.origin)) {
        console.log('Third-party:', {
          url: resource.name,
          duration: resource.duration,
          transferSize: resource.transferSize,
          initiatorType: resource.initiatorType,
        });
      }
    });
  });
  
  observer.observe({ entryTypes: ['resource'] });
}

Lighthouse

npx lighthouse https://example.com --view

# Check "Reduce JavaScript execution time"
# Third-party scripts listed with impact

Common Third-Parties

Google Analytics

// Load after page interactive
<Script
  src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
  strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
  {`
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', 'GA_ID');
  `}
</Script>

Facebook Pixel

<Script id="facebook-pixel" strategy="afterInteractive">
  {`
    !function(f,b,e,v,n,t,s)
    {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
    n.callMethod.apply(n,arguments):n.queue.push(arguments)};
    if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
    n.queue=[];t=b.createElement(e);t.async=!0;
    t.src=v;s=b.getElementsByTagName(e)[0];
    s.parentNode.insertBefore(t,s)}(window, document,'script',
    'https://connect.facebook.net/en_US/fbevents.js');
    fbq('init', 'PIXEL_ID');
    fbq('track', 'PageView');
  `}
</Script>

Intercom / Chat Widgets

// Load on interaction
function IntercomChat() {
  const [loaded, setLoaded] = useState(false);

  const loadIntercom = () => {
    if (loaded) return;
    
    (window as any).Intercom('boot', {
      app_id: 'APP_ID',
    });
    
    setLoaded(true);
  };

  return (
    <button onClick={loadIntercom}>
      Open Chat
    </button>
  );
}

Best Practices

  1. Load After Interactive: Use strategy="afterInteractive"
  2. Async Always: Never block main thread
  3. Lazy Load: Load on interaction or visibility
  4. Use Facades: YouTube, maps, social embeds
  5. Self-Host: When possible
  6. Web Worker: Use Partytown for heavy scripts
  7. Resource Hints: DNS prefetch, preconnect
  8. Monitor: Track third-party impact
  9. CSP: Restrict what can load
  10. Audit Regularly: Remove unused scripts

Decision Framework

Is it critical for initial render?
  ├─ Yes → strategy="beforeInteractive" (rare!)
  └─ No
      ├─ Is it needed immediately after page load?
      │   ├─ Yes → strategy="afterInteractive"
      │   └─ No
      │       ├─ Does user need to interact first?
      │       │   ├─ Yes → Load on interaction
      │       │   └─ No → strategy="lazyOnload"

      └─ Can it run in web worker?
          ├─ Yes → Use Partytown
          └─ No → Defer to idle

Common Pitfalls

Loading in <head>: Blocks rendering
Load after page interactive

Sync loading: Blocks parsing
Always async

All third-parties at once: Overwhelms network
Prioritize and stagger

No monitoring: Can't optimize
Track impact in production

Trusting third-parties: They can change
Self-host when possible

Third-party scripts are the #1 performance killer—manage them aggressively and see dramatic improvements!

On this page