<Script>

Loads a third-party script at a chosen point in the page lifecycle instead of blocking rendering.

Signature
import Script from 'next/script'

<Script
  src={string}   // required unless an inline script is used
  strategy={'beforeInteractive' | 'afterInteractive' | 'lazyOnload' | 'worker'} // default: 'afterInteractive'
  onLoad={() => void}
  onReady={() => void}
  onError={(error: Error) => void}
/>
  • Added in Next.js v11.0.0

Parameters

ParameterTypeDescription
$srcoptionalstringAbsolute external URL or internal path of the script. Required unless the script body is supplied inline.
$strategyoptional'beforeInteractive' | 'afterInteractive' | 'lazyOnload' | 'worker'= 'afterInteractive'When to load: beforeInteractive loads from the server HTML ahead of any Next.js module, afterInteractive loads client-side after some hydration, lazyOnload waits for browser idle time after all other resources, worker offloads to a web worker (experimental).
$onLoadoptional() => voidRuns once, immediately after the script finishes loading. Client Components only, and unavailable with beforeInteractive.
$onReadyoptional() => voidRuns after the script loads and again on every subsequent mount of the component, which is what you want for embeds that must be re-instantiated after a navigation. Client Components only.
$onErroroptional(error: Error) => voidRuns when the script fails to load. Client Components only, and cannot be combined with beforeInteractive.

Return value

JSX.ElementNothing renders visibly; the component injects a <script> element at the point determined by strategy.

Errors

Next.js errors when onLoad or onError is combined with strategy="beforeInteractive", and all three handler props error unless the surrounding component is a Client Component.

Changelog

  • PHP v13.0.0beforeInteractive and afterInteractive were reworked to support the App Router; beforeInteractive must now live in a root layout.
  • PHP v12.2.4onReady prop added.
  • PHP v12.2.2beforeInteractive scripts allowed in _document (Pages Router).

<Script> exists so a third-party tag does not sit in the critical path. It deduplicates the script across navigations and lets you say when it should run rather than where it happens to sit in the markup.

Picking a strategy

  • beforeInteractive — bot detectors and cookie consent managers, the handful of scripts that must observe the page before anything else. It must be placed in a root layout and is always injected into <head>, wherever you write it.
  • afterInteractive (default) — analytics and tag managers.
  • lazyOnload — chat widgets and social embeds that can wait for idle time.
  • worker — experimental, requires experimental.nextScriptWorkers, and does not work in the App Router yet.

Where people get stuck

The handler props are client-only: any file using onLoad, onReady or onError needs 'use client'. Prefer onReady over onLoad for anything that must be re-initialised after a client-side navigation, because onLoad fires once per document. A beforeInteractive script also runs once per document load — navigating between routes will not run it again, not even a change of root param such as /en to /fi.

Example

Example · typescript
import Script from 'next/script'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://cdn.example.com/consent-manager.js"
          strategy="beforeInteractive"
        />
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
          strategy="afterInteractive"
        />
      </body>
    </html>
  )
}

When to use it

  • Load a cookie consent manager before any other script can set a cookie.
  • Add a Google Analytics or Tag Manager snippet without delaying hydration.
  • Defer a chat support widget until the browser is idle.
  • Re-instantiate a map embed each time the component mounts after a client-side navigation.

Frequently asked questions

Why does onLoad throw an error in my component?

Either the file is a Server Component - the handler props require 'use client' - or the strategy is beforeInteractive, which does not support onLoad or onError. Use onReady for beforeInteractive-style needs.

Where do I put a beforeInteractive script in the App Router?

In a root layout such as app/layout.tsx. It is injected into <head> regardless of its position in the JSX, and it runs once per document load, not per client-side navigation.

Does the worker strategy work in the App Router?

No. The worker strategy is experimental, needs experimental.nextScriptWorkers in next.config.js, and currently only works in the pages/ directory.

More examples

An inline snippet with an id

The second <Script> has no src, so it carries its body as children and an id becomes mandatory - that id is how Next.js knows the snippet has already been injected. Both scripts share a strategy so the config call cannot run before the library it configures.

Example · typescript
import Script from 'next/script'

export default function Analytics({ gaId }: { gaId: string }) {
  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
        strategy="afterInteractive"
      />
      <Script id="gtag-init" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', '${gaId}');
        `}
      </Script>
    </>
  )
}

onReady for an embed that must be rebuilt on every mount

onLoad would build the map once and leave an empty div after the user navigated away and back, because the script is already loaded the second time. onReady fires on that remount too. The 'use client' directive is required here purely because these handlers are functions passed as props.

Example · typescript
'use client'

import { useRef } from 'react'
import Script from 'next/script'

export default function MapPanel() {
  const mapRef = useRef<HTMLDivElement>(null)

  return (
    <>
      <div ref={mapRef} style={{ height: 400 }} />
      <Script
        id="google-maps"
        src="https://maps.googleapis.com/maps/api/js"
        onReady={() => {
          new google.maps.Map(mapRef.current!, {
            center: { lat: -34.397, lng: 150.644 },
            zoom: 8,
          })
        }}
        onError={(error) => {
          console.error('Maps failed to load', error)
        }}
      />
    </>
  )
}

Deferring a low-priority widget

lazyOnload holds the request until every other resource has been fetched and the browser is idle, so the widget cannot compete with the page's own JavaScript. Because this <Script> lives in a page rather than the root layout, it is only requested on routes that actually render it.

Example · typescript
import Script from 'next/script'

export default function SupportPage() {
  return (
    <>
      <h1>Support</h1>
      <Script
        src="https://widget.example.com/chat.js"
        strategy="lazyOnload"
      />
    </>
  )
}

Discussion

  • Be the first to comment on this lesson.