Skip to content

Script Optimization

Third-party scripts (analytics, ads, chat widgets) are a common source of performance problems. The next/script component gives you control over when and how scripts load, preventing them from blocking page rendering.

A typical analytics script:

<script src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>

Without optimization, this script blocks rendering — the page can’t display content until the script downloads and executes. next/script lets you defer this until after the page is interactive.

import Script from 'next/script'
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
{children}
{/* After — loads after the page is interactive (recommended) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
strategy="afterInteractive"
/>
{/* Before — loads before the page is interactive (use sparingly) */}
<Script
src="https://cdn.example.com/critical.js"
strategy="beforeInteractive"
/>
{/* Lazy — loads when the browser is idle */}
<Script
src="https://cdn.example.com/chat-widget.js"
strategy="lazyOnload"
/>
{/* Worker — executes in a web worker (experimental) */}
<Script
src="https://cdn.example.com/heavy-task.js"
strategy="worker"
/>
</>
)
}
StrategyWhen Script LoadsWhen It ExecutesUse Case
beforeInteractiveIn the initial HTMLBefore page is interactiveCritical polyfills, feature detection
afterInteractiveAfter page is interactiveAs soon as possibleAnalytics, tag managers
lazyOnloadDuring browser idle timeWhen browser is freeChat widgets, social embeds
workerDuring idle timeIn a web workerExpensive processing (Partytown)
<Script id="show-banner" strategy="afterInteractive">
{`document.getElementById('banner').classList.remove('hidden')`}
</Script>

Inline scripts require an id attribute for Next.js to track and optimize them.

<Script
src="https://cdn.example.com/widget.js"
strategy="lazyOnload"
onLoad={() => {
console.log('Widget loaded and ready')
}}
onError={(e) => {
console.error('Widget failed to load')
}}
/>
app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
{/* Google Analytics */}
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_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', '${process.env.NEXT_PUBLIC_GA_ID}');
`}
</Script>
</body>
</html>
)
}
  • Using beforeInteractive unnecessarily — Most scripts should use afterInteractive. Only use beforeInteractive for critical polyfills.
  • Not adding event handlers — Track failures to know when third-party scripts break.
  • Loading scripts in child components — Load scripts in layouts to avoid duplication on route changes.
  • Not using environment variables for IDs — Hardcoding tracking IDs makes them harder to manage across environments.
  • Use afterInteractive for analytics and marketing scripts
  • Use lazyOnload for non-critical scripts (chat, social widgets)
  • Load scripts in the root layout to avoid duplication
  • Add onError handlers to track script failures
  • Use environment variables for API keys and tracking IDs

next/script gives you control over third-party script loading. Use afterInteractive for analytics, lazyOnload for non-critical widgets, and beforeInteractive only when absolutely necessary. This prevents scripts from blocking page rendering and improves performance.