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.
Outcome
Third-party scripts loaded with optimal strategies: critical scripts before interaction, analytics after interaction, and non-essential widgets lazy-loaded.
Fast Track
- Replace raw
<script>tags with Next.jsScriptcomponent - Choose appropriate
strategyprop for each script - Verify main thread is not blocked during page load
Script Loading Strategies
Next.js provides four loading strategies for third-party scripts, each with different performance trade-offs.
beforeInteractive (Use Sparingly)
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.
afterInteractive (Recommended Default)
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)
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)
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
| Strategy | Load Time | Blocks Rendering | Use Cases |
|---|---|---|---|
beforeInteractive | Before hydration | ✅ Yes | Critical polyfills only |
afterInteractive | After interactive | ❌ No | Analytics, chat, payments |
lazyOnload | After everything | ❌ No | Social widgets, comments |
worker | Off main thread | ❌ No | Experimental, 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
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:
- Find the raw
<script>tags in the layout (Google Analytics) - Create a Client Component for the analytics scripts (required for
onLoadcallbacks) - Replace raw scripts with Next.js
Scriptcomponent - Apply
strategy="afterInteractive"for analytics - Add
onLoadcallback to verify script loaded - Measure impact on Time to Interactive (TTI)
Implementation hints:
onLoad,onReady, andonErrorcallbacks only work in Client Components- Create
apps/web/src/components/google-analytics.tsxwith'use client'directive - Move analytics scripts to
afterInteractive(notbeforeInteractive) - Use
lazyOnloadfor anything below the fold - Add
onLoadcallbacks 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'
}Try It
-
Test page load without scripts:
- Open Chrome DevTools → Performance
- Record page load
- Check main thread activity
-
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
-
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-scriptsDone-When
- Layout uses
import Script from "next/script" - Raw
<script>tags replaced with<Script>component - Both scripts use
strategy="afterInteractive" - Inline script has
idprop (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
'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
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:
- Created
GoogleAnalyticsClient Component with'use client'directive - Moved Script imports to the client component
- Replaced raw
<script>with<Script>component - Added
strategy="afterInteractive"to defer loading - Added
idprop to inline script (required for inline scripts) - Added
onLoadcallback 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:
- Open Chrome DevTools → Performance tab
- Record page load
- Confirm main thread is not blocked by analytics script
- Check Console for "Google Analytics script loaded" message
Script Loading Strategy:
- Audit: Find all third-party scripts
- Classify: Critical, important, or optional
- Apply strategy:
- Critical →
beforeInteractive(rare) - Important →
afterInteractive(default) - Optional →
lazyOnload(aggressive)
- Critical →
- 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
Was this helpful?