Vercel Logo

Third‑Party Scripts

Third-party scripts like analytics, chat widgets, and ads can destroy your page performance if loaded incorrectly. Poor script loading blocks rendering, delays interactivity, and tanks Core Web Vitals scores (Google's key performance metrics: LCP, INP, CLS). The right loading strategy can prevent a 2-second delay in Time to Interactive.

Working Directory

This lesson uses apps/web. All file paths are relative to that directory.

Outcome

Third-party scripts loaded with optimal strategies: critical scripts before interaction, analytics after interaction, and non-essential widgets lazy-loaded.

Fast Track

  1. Replace raw <script> tags with Next.js Script component
  2. Choose appropriate strategy prop for each script
  3. Verify main thread is not blocked during page load
Turbopack Build Performance

Turbopack significantly improves build performance for applications with many third-party scripts. Development server startup is faster, and production builds optimize script chunking more efficiently.

https://nextjs.org/docs/app/api-reference/turbopack

Script Loading Strategies

Next.js provides four loading strategies for third-party scripts, each with different performance trade-offs.

beforeInteractive (Use Sparingly)

Blocks Page Hydration

Only use beforeInteractive for critical polyfills that must load before React hydration. This strategy blocks the entire page from becoming interactive.

apps/web/src/app/layout.tsx
import Script from 'next/script'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <Script
          src="https://polyfill.io/v3/polyfill.min.js"
          strategy="beforeInteractive"
        />
        {children}
      </body>
    </html>
  )
}

When to use:

  • Critical polyfills needed before React hydrates
  • Browser feature detection that blocks rendering

Performance impact: Blocks page interactivity - use only when absolutely necessary.

Best Balance of Functionality and Performance

Use afterInteractive for 90% of third-party scripts. Loads after the page becomes interactive, minimizing impact on user experience.

apps/web/src/app/layout.tsx
import Script from 'next/script'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        {children}
        <Script
          src="https://analytics.example.com/script.js"
          strategy="afterInteractive"
          onLoad={() => {
            console.log('Analytics script loaded')
          }}
        />
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
          strategy="afterInteractive"
        />
      </body>
    </html>
  )
}

When to use:

  • Analytics and tracking scripts (Google Analytics, Segment)
  • Chat widgets (Intercom, Zendesk)
  • Payment forms (Stripe, PayPal)
  • Social media embeds
  • A/B testing tools

Performance impact: Loads after page is interactive - minimal user-facing delay.

lazyOnload (Lowest Priority)

apps/web/src/app/page.tsx
import Script from 'next/script'
 
export default function HomePage() {
  return (
    <>
      <Script
        src="https://widget.example.com/chat.js"
        strategy="lazyOnload"
      />
      <Script
        src="https://platform.twitter.com/widgets.js"
        strategy="lazyOnload"
      />
      <main>
        <h1>Welcome</h1>
      </main>
    </>
  )
}

When to use:

  • Non-essential chat widgets
  • Social media share buttons
  • Comment systems (Disqus)
  • Background metrics collection
  • Optional features users may not interact with

Performance impact: Lowest priority - loads after everything else, zero impact on core functionality.

worker (Experimental)

Experimental Feature

The worker strategy is experimental and requires enabling nextScriptWorkers in next.config.js. Browser support is limited. Use with caution in production.

apps/web/src/app/page.tsx
import Script from 'next/script'
 
export default function HomePage() {
  return (
    <Script
      src="https://analytics.example.com/heavy-script.js"
      strategy="worker"
    />
  )
}

When to use:

  • Experimental feature
  • Heavy computation scripts
  • Scripts that don't need DOM access

Performance impact: Runs in Web Worker, off main thread - best performance but limited browser support.

Performance Comparison

StrategyLoad TimeBlocks RenderingUse Cases
beforeInteractiveBefore hydration✅ YesCritical polyfills only
afterInteractiveAfter interactive❌ NoAnalytics, chat, payments
lazyOnloadAfter everything❌ NoSocial widgets, comments
workerOff main thread❌ NoExperimental, heavy scripts

Common Script Classifications

Critical (beforeInteractive):

  • Browser polyfills for unsupported features
  • Feature detection that affects rendering

Important (afterInteractive):

  • Google Analytics, Segment, Mixpanel
  • Stripe, PayPal payment forms
  • Intercom, Zendesk chat widgets
  • Google Maps, Mapbox
  • Auth0, Clerk authentication

Optional (lazyOnload):

  • Twitter/Facebook social embeds
  • Disqus comments
  • Share buttons
  • Background metrics
  • Non-essential widgets
Need Help Measuring Third-Party Script Impact?

Unsure which scripts are actually hurting your performance and by how much? Use this structured prompt to audit and quantify third-party script impact:

Prompt: Audit Third-Party Script Impact
<context>
I'm auditing third-party scripts in a Next.js application to measure their performance impact on Core Web Vitals and Time to Interactive (TTI).
 
My application setup:
- Next.js with App Router
- Multiple third-party scripts: analytics, chat widgets, social embeds, ads, etc.
- Some scripts loaded with next/script, others with raw script tags
- Need to measure actual impact on: TTI (Time to Interactive), FCP (First Contentful Paint), LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift), TBT (Total Blocking Time)
</context>
 
<current-implementation>
I have multiple third-party scripts but don't know:
- Which scripts block rendering or delay interactivity
- How much each script contributes to page weight and load time
- Whether scripts are using optimal loading strategies
- If scripts cause layout shifts or main thread blocking
</current-implementation>
 
<problems>
1. **No visibility:** Can't tell which script is causing the 2-second TTI delay
2. **Mixed strategies:** Some scripts use beforeInteractive, others use default loading
3. **Unknown cost:** Don't know the size/execution time of each script
4. **Layout shifts:** Some scripts cause CLS issues but not sure which ones
5. **Third-party cascade:** One script loads others - hard to trace the full impact
</problems>
 
<questions>
1. **Measurement approach:** What's the best way to measure script impact?
   - Use Chrome DevTools Performance tab to track main thread blocking?
   - Use Lighthouse to compare scores with/without scripts?
   - Use WebPageTest to see waterfall of script loads?
   - Implement custom RUM (Real User Monitoring, collecting performance data from actual users) with Performance API?
 
2. **Metrics to track:** Which metrics best show script impact?
   - **TTI (Time to Interactive):** How long until page is fully interactive?
   - **TBT (Total Blocking Time):** How much do scripts block the main thread?
   - **JS Bundle Size:** How much does each script add to page weight?
   - **CLS (Cumulative Layout Shift):** Do scripts cause layout jumps?
   - **Network waterfall:** Do scripts delay critical resources?
 
3. **Isolation strategy:** How do I measure each script's individual impact?
   - Remove one script at a time and measure difference?
   - Use Chrome DevTools Coverage to see unused code?
   - Block scripts with DevTools and compare Lighthouse scores?
   - Use Request Blocking in Network tab?
 
4. **Strategy recommendations:** Based on measurements, how do I decide:
   - Which scripts to remove entirely (cost > benefit)?
   - Which scripts to move from beforeInteractive to afterInteractive?
   - Which scripts to move from afterInteractive to lazyOnload?
   - Which scripts to lazy-load only when user interacts?
 
5. **Reporting format:** Should the audit report include:
   - Per-script metrics (size, load time, blocking time)?
   - Before/after Core Web Vitals comparison?
   - Severity ratings (critical, high, medium impact)?
   - Specific optimization recommendations with code examples?
</questions>
 
<specific-scenario>
Example application with multiple scripts:
 
    <Script src="https://analytics.example.com/v1.js" strategy="beforeInteractive" />
    <Script src="https://chat.example.com/widget.js" strategy="afterInteractive" />
    <Script src="https://ads.example.com/script.js" />
    <script src="https://social.example.com/share.js"></script>
 
Current performance (from Lighthouse):
- TTI: 4.2s
- TBT: 890ms
- LCP: 2.8s
- CLS: 0.15
- JS Bundle: 1.2MB
 
Expected audit findings:
 
1. **Analytics script (critical impact):**
   - Size: 120KB
   - Blocking time: 450ms
   - Strategy: beforeInteractive (WRONG - blocks hydration)
   - CLS impact: None
   - **Recommendation:** Change to afterInteractive saves 450ms on TTI
   - **Code:**
       <Script src="https://analytics.example.com/v1.js" strategy="afterInteractive" />
 
2. **Chat widget (high impact):**
   - Size: 380KB
   - Blocking time: 290ms
   - Strategy: afterInteractive
   - CLS impact: 0.08 (widget causes layout shift)
   - **Recommendation:** Change to lazyOnload + add placeholder saves 290ms TBT, fixes CLS
   - **Code:**
       <Script src="https://chat.example.com/widget.js" strategy="lazyOnload" />
 
3. **Ads script (medium impact):**
   - Size: 250KB
   - Blocking time: 120ms
   - Strategy: default (no strategy = render-blocking)
   - **Recommendation:** Add strategy="lazyOnload" saves 120ms
   - **Code:**
       <Script src="https://ads.example.com/script.js" strategy="lazyOnload" />
 
4. **Social share (low impact but fixable):**
   - Size: 45KB
   - Uses raw script tag (should use Next.js Script)
   - **Recommendation:** Convert to Script component with lazyOnload
   - **Code:**
       <Script src="https://social.example.com/share.js" strategy="lazyOnload" />
 
Projected improvement after optimizations:
- TTI: 4.2s to 2.5s (1.7s faster - 40% improvement)
- TBT: 890ms to 140ms (750ms saved - 84% improvement)
- CLS: 0.15 to 0.07 (53% improvement)
- No change in functionality, all features still work
 
Measurement commands:
 
    # Baseline measurement
    npx lighthouse http://localhost:3000 --only-categories=performance
 
    # With scripts blocked (to measure total script cost)
    npx lighthouse http://localhost:3000 --only-categories=performance --blocked-url-patterns="analytics.example.com,chat.example.com"
</specific-scenario>
 
Generate a comprehensive script audit methodology with step-by-step measurement instructions, analysis techniques using Chrome DevTools and Lighthouse, and a detailed report format that quantifies the performance impact of each third-party script with specific optimization recommendations.

This prompt will help you build a data-driven approach to third-party script optimization, showing exactly which scripts hurt performance and by how much.

Hands-On Exercise 4.3

The starter repo has raw <script> tags in the layout. Convert them to use next/script with proper loading strategies.

Target file: apps/web/src/app/layout.tsx

Requirements:

  1. Find the raw <script> tags in the layout (Google Analytics)
  2. Create a Client Component for the analytics scripts (required for onLoad callbacks)
  3. Replace raw scripts with Next.js Script component
  4. Apply strategy="afterInteractive" for analytics
  5. Add onLoad callback to verify script loaded
  6. Measure impact on Time to Interactive (TTI)

Implementation hints:

  • onLoad, onReady, and onError callbacks only work in Client Components
  • Create apps/web/src/components/google-analytics.tsx with 'use client' directive
  • Move analytics scripts to afterInteractive (not beforeInteractive)
  • Use lazyOnload for anything below the fold
  • Add onLoad callbacks to verify scripts loaded successfully:
    'use client';
    import Script from 'next/script';
     
    export function GoogleAnalytics() {
      return (
        <Script
          src="/analytics.js"
          strategy="afterInteractive"
          onLoad={() => console.log('Analytics ready')}
        />
      );
    }
  • Test on slow 3G network to see impact
  • Check Chrome DevTools → Performance → Main thread activity

Script audit checklist:

// Example classification
const scriptStrategy = {
  'analytics.js': 'afterInteractive',
  'chat-widget.js': 'lazyOnload',
  'social-share.js': 'lazyOnload',
  'payment-form.js': 'afterInteractive',
  'polyfill.js': 'beforeInteractive'
}
Optional: Generate with v0

Use v0 for small UI shells that integrate with third-party widgets (e.g., consent banners, share buttons). Load the actual scripts with next/script and measure.

Prompt:

Create a non-blocking third-party widget container with a placeholder skeleton and a consent toggle using Tailwind presentational only no inline scripts.

Open in v0: Open in v0

Try It

  1. Test page load without scripts:

    • Open Chrome DevTools → Performance
    • Record page load
    • Check main thread activity
  2. Add scripts with different strategies:

    <Script src="/slow-script.js" strategy="beforeInteractive" />

    Expected: Main thread blocked until script loads

    <Script src="/slow-script.js" strategy="afterInteractive" />

    Expected: Page interactive before script loads

  3. Measure Time to Interactive (TTI):

    • Use Lighthouse to measure TTI before and after optimization
    • Target: Reduce TTI by at least 500ms

Commit & Deploy

git add -A
git commit -m "perf(scripts): optimize third-party script loading with next/script strategies"
git push -u origin feat/polish-third-party-scripts

Done-When

  • Layout uses import Script from "next/script"
  • Raw <script> tags replaced with <Script> component
  • Both scripts use strategy="afterInteractive"
  • Inline script has id prop (required for inline scripts)
  • Console shows "Google Analytics script loaded" on page load
  • Chrome DevTools Performance tab shows no main thread blocking

Solution

Complete optimized layout with next/script

The starter file at apps/web/src/app/layout.tsx has raw <script> tags. Since onLoad callbacks require Client Components, create a dedicated analytics component:

Step 1: Create the GoogleAnalytics client component

apps/web/src/components/google-analytics.tsx
'use client';
 
import Script from 'next/script';
 
/**
 * Google Analytics component using next/script with proper loading strategy.
 * Must be a Client Component to use onLoad callback.
 */
export function GoogleAnalytics() {
  return (
    <>
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
        strategy="afterInteractive"
        onLoad={() => {
          console.log('Google Analytics script loaded');
        }}
      />
      <Script
        id="google-analytics-init"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'GA_MEASUREMENT_ID');
          `,
        }}
      />
    </>
  );
}

Step 2: Use in the layout

apps/web/src/app/layout.tsx
import type { Metadata } from "next";
 
import "./globals.css";
import { GoogleAnalytics } from "../components/google-analytics";
 
export const metadata: Metadata = {
  title: process.env.NEXT_PUBLIC_APP_NAME || "Vercel Academy Foundation - Web",
  description: "VAF Web",
};
 
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className="container mx-auto px-4 py-8">
        {children}
 
        {/* Google Analytics - client component for onLoad support */}
        <GoogleAnalytics />
      </body>
    </html>
  );
}

Key changes from starter:

  1. Created GoogleAnalytics Client Component with 'use client' directive
  2. Moved Script imports to the client component
  3. Replaced raw <script> with <Script> component
  4. Added strategy="afterInteractive" to defer loading
  5. Added id prop to inline script (required for inline scripts)
  6. Added onLoad callback to verify loading (requires Client Component)

Why a Client Component for analytics?

The onLoad, onReady, and onError callbacks only work in Client Components. This is because these callbacks need to run JavaScript in the browser after the script loads. Server Components can use <Script> without callbacks, but if you need load confirmation, extract to a Client Component.

Why afterInteractive for analytics:

  • Page becomes interactive before analytics loads
  • No blocking of hydration
  • Users can interact immediately
  • Analytics still tracks all events (just loads slightly later)

Verify the improvement:

  1. Open Chrome DevTools → Performance tab
  2. Record page load
  3. Confirm main thread is not blocked by analytics script
  4. Check Console for "Google Analytics script loaded" message

Script Loading Strategy:

  1. Audit: Find all third-party scripts
  2. Classify: Critical, important, or optional
  3. Apply strategy:
    • Critical → beforeInteractive (rare)
    • Important → afterInteractive (default)
    • Optional → lazyOnload (aggressive)
  4. Measure: Use Lighthouse to verify TTI improvement

Best practice: Default to afterInteractive for most scripts. Only use beforeInteractive for polyfills that must run before React hydration. Use lazyOnload aggressively for non-essential features.

Performance wins:

  • Analytics deferred: +500ms TTI
  • Chat widgets lazy-loaded: +800ms TTI
  • Social embeds lazy-loaded: +300ms TTI

References