Type something to search...
Next.js Script Component

Next.js Script Component

If you've read a guide on optimizing third-party scripts in Next.js, you already know the pitch: next/script gives you a strategy prop instead of dropping a raw <script> tag and hoping for the best. What guides tend to skip over is the actual API surface — what every prop does, which ones only work in Client Components, which combinations Next.js will quietly refuse to let you use, and what changed as the component matured from Next.js 11 through today. That's what this article covers: the <Script /> component as a reference, prop by prop, rather than as a strategy-selection tutorial.

This matters because the failure mode with next/script is rarely "I don't know which strategy to pick." It's things like: wiring up onLoad inside a Server Component and getting a build error, forgetting that beforeInteractive scripts always end up in <head> regardless of where you render the component, or reaching for worker and discovering it doesn't actually work in the App Router yet. None of that is obvious from a high-level strategy guide — it's the kind of thing you only learn by reading the prop reference closely, or by hitting the error yourself.

Import and Basic Usage

next/script extends the native HTML <script> element, so the mental model is: everything you'd normally put in a <script src="..."> tag, plus a handful of Next.js-specific props that control when and how it loads.

import Script from "next/script";

export default function Dashboard() {
  return (
    <>
      <Script src="https://example.com/script.js" />
    </>
  );
}

With no other props, this behaves close to a default <script> tag loaded client-side — but as soon as you add strategy, you're opting into Next.js's loading orchestration instead of the browser's default parse-and-execute-in-order behavior.

The Props at a Glance

PropExampleTypeRequired
srcsrc="http://example.com/script"StringRequired unless an inline script is used
strategystrategy="lazyOnload"StringNo — defaults to afterInteractive
onLoadonLoad={onLoadFunc}FunctionNo
onReadyonReady={onReadyFunc}FunctionNo
onErroronError={onErrorFunc}FunctionNo

That's the documented, Next.js-specific surface. Because Script wraps a real <script> element, standard HTML attributes pass straight through too — id, nonce, crossOrigin, integrity, async, defer, and dangerouslySetInnerHTML for inline content all work exactly as they would on a plain tag. The five props above are the ones Next.js adds meaning to; everything else behaves like ordinary JSX on a <script> element.

Required Props

src

A path string pointing at the script — either an absolute external URL (https://cdn.example.com/widget.js) or an internal path served from your own public/ directory. src is the only prop that's conditionally required: you can skip it if you're rendering an inline script instead.

<Script id="inline-example">
  {`console.log('hello from an inline script')`}
</Script>

For inline scripts, give them an id — Next.js uses it to track and dedupe the script across re-renders, and without one you'll get a runtime warning.

Optional Props

strategy

This is the prop that actually differentiates next/script from a plain tag. Four values are documented, and picking the right one is entirely about when in the page lifecycle the script is allowed to run.

beforeInteractive — injected into the initial server-rendered HTML and fetched before any Next.js module, executed in placement order. Critically, execution does not block hydration — it just means the script is available extremely early. Next.js enforces that beforeInteractive scripts can only be placed in a root layout (app/layout.tsx), and it will physically move them into the document <head> regardless of where you actually render the <Script> component in your JSX. If you go looking for the script tag in the DOM and it's not where you put it in code, this is why.

// app/layout.tsx
import Script from "next/script";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script
          src="https://example.com/script.js"
          strategy="beforeInteractive"
        />
      </body>
    </html>
  );
}

Two behavioral details worth internalizing: these scripts run once per full document load, not once per route — a client-side navigation, even one that only swaps a root param like /en to /fi, does not re-run them, because the root layout persists across that navigation. This makes beforeInteractive a poor fit for anything that needs fresh state per page; it's meant for things that should exist exactly once, like a bot-detection script or a cookie-consent manager that needs to be live before the user can interact with anything.

afterInteractive (the default) — injected client-side, loading after some or all hydration has occurred. If you never set strategy explicitly, this is what you get. Unlike beforeInteractive, these scripts can live inside any page or layout, and they only load when that particular page (or layout subtree) is actually mounted in the browser — so a script inside app/dashboard/page.tsx won't load until someone visits /dashboard. This is the right default for tag managers and analytics: important enough to load promptly, but not so critical it needs to block the initial paint.

lazyOnload — deferred until the browser is idle, after everything else on the page has already loaded. Like afterInteractive, it's scoped to wherever you place the component. This is the strategy for scripts nobody is waiting on: chat widgets, social share buttons, anything a user might never even interact with during their visit.

worker (experimental) — offloads execution to a Web Worker via Partytown, freeing the main thread entirely. The catch, and it's a significant one: the docs currently state this doesn't work in the App Router, and it can only be used today inside a pages/ directory. If you're on a pure App Router project, this strategy simply isn't available to you yet regardless of how appealing "get third-party JS off the main thread" sounds. It also requires an explicit opt-in flag:

// next.config.js
module.exports = {
  experimental: {
    nextScriptWorkers: true,
  },
};

onLoad

Runs once, the first time the script finishes loading. This is for one-time initialization — instantiating a library, reading a value it exposes on window, that kind of thing.

"use client";

import Script from "next/script";

export default function Page() {
  return (
    <Script
      src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"
      onLoad={() => {
        console.log(_.sample([1, 2, 3, 4]));
      }}
    />
  );
}

Two hard constraints here that will bite you if you don't know them upfront: onLoad does not work in Server Components — the component using it must be a Client Component ('use client' at the top of the file), because event handler props like this one need to exist on the client to be attached at all. And onLoad cannot be combined with strategy="beforeInteractive" — the docs point you toward onReady instead for that case. If you try to pass onLoad on a beforeInteractive script, you're working against the component's own design rather than with it.

onReady

The distinction between onReady and onLoad is easy to miss and important: onReady fires on the initial load and every time the component remounts afterward — for example, after a client-side navigation brings the component back into the tree. onLoad only ever fires once, on first load.

That makes onReady the correct choice for anything that needs to reinitialize per-mount — the docs' own example is re-instantiating a Google Maps embed every time its container remounts:

"use client";

import { useRef } from "react";
import Script from "next/script";

export default function Page() {
  const mapRef = useRef();

  return (
    <>
      <div ref={mapRef}></div>
      <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,
          });
        }}
      />
    </>
  );
}

Same Client Component requirement as onLoad applies here — no Server Components allowed.

onError

Fires when the script fails to load entirely — a 404, a network failure, a blocked request from an ad blocker or restrictive CSP. This is the one prop of the three that has no interaction with the beforeInteractive restriction mentioned for onLoad... except it does, just in the opposite direction: onError cannot be used with beforeInteractive either, and like the other two callbacks, it only works in Client Components.

"use client";

import Script from "next/script";

export default function Page() {
  return (
    <Script
      src="https://example.com/script.js"
      onError={(e: Error) => {
        console.error("Script failed to load", e);
      }}
    />
  );
}

In practice, onError is worth wiring up for any third-party script your application has a fallback behavior for — a payment widget, a maps embed, anything where "silently missing" is worse than "visibly broken with a retry option."

Why the Client Component Restriction Exists

It trips people up the first time, so it's worth explaining rather than just stating: onLoad, onReady, and onError are all function props. Server Components render to a serializable payload that gets sent to the client — you can't serialize a JavaScript closure across that boundary, so any prop that's a function simply can't be passed from a Server Component to a component instance that needs to invoke it client-side. The practical fix is exactly what the examples above show: put 'use client' at the top of whichever file renders the <Script> tag with a callback prop. If you only need src and strategy, you can keep the component as a Server Component — it's specifically the callback props that force the client boundary.

Common Mistakes

Reaching for worker in the App Router. The docs are explicit that this strategy doesn't function there yet. If you need main-thread relief for a heavy third-party script in an App Router project, you don't currently have this lever — look at lazyOnload combined with genuinely deferring the script's own internal work instead.

Expecting beforeInteractive scripts to re-run on navigation. They don't, by design — they're tied to the document load, not the route. If your script needs to reinitialize per-page, it isn't a beforeInteractive candidate at all; you want afterInteractive or lazyOnload with onReady instead.

Passing onLoad without 'use client'. This produces a build-time or runtime error depending on your setup, and the message doesn't always make the actual cause obvious on first read. If you see an error mentioning event handlers and Server Components pointing at a file with a <Script> tag, check for a missing 'use client' directive first.

Forgetting id on inline scripts. Without one, Next.js can't track the script across re-renders reliably, and you'll see a console warning nudging you to add it.

Version History

The component has been stable for a while, but a few milestones are worth knowing if you're reading older code or Stack Overflow answers:

VersionChange
v13.0.0beforeInteractive and afterInteractive updated to support the app directory
v12.2.4onReady added
v12.2.2beforeInteractive scripts allowed inside _document (Pages Router)
v11.0.0next/script introduced

If you're reading a tutorial that predates v12.2.4, it won't mention onReady at all — that's a meaningful gap if the tutorial's example needs per-mount reinitialization and only shows onLoad.

Key Takeaways

PropClient Component required?Works with beforeInteractive?
srcNoYes
strategyNo
onLoadYesNo
onReadyYesYes
onErrorYesNo

The pattern worth remembering: strategy and src are layout-agnostic and Server-Component-safe. The three callbacks — onLoad, onReady, onError — all require a Client Component, and two of the three explicitly refuse to pair with beforeInteractive. Once you've internalized that split, the rest of the API is just picking the right strategy for how urgently a given script needs to run, which the Guides-level walkthrough on optimizing scripts covers in more depth.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper