next/script: Three Scripts, Three Mechanisms, One CSP Bill
This layout loads three scripts and only one is a <Script>. A census of which mechanism each needs, and what every script costs your CSP.
next/script is Next.js's wrapper around <script>, and its whole value is the strategy prop: beforeInteractive, afterInteractive (the default) or lazyOnload, which decide when a third-party script loads relative to hydration. This storefront's root layout loads three scripts by three different mechanisms, and only one of them uses <Script> — the other two are deliberate exceptions.
That split is the useful part, because the question in practice is never "how do I use next/script" but "which of these three scripts should be a <Script>, and what happens to my Content-Security-Policy when I decide".
The three scripts in one layout, and why they differ
Every script this site loads is declared in src/app/layout.tsx. Here is the census:
| Script | Mechanism | Why not the other options |
|---|---|---|
| LemonSqueezy checkout overlay | <Script strategy="afterInteractive"> | Third-party, needed only once a buyer clicks — the textbook next/script case |
| Theme bootstrap (dark mode) | Raw <script dangerouslySetInnerHTML> in <head> | Must run before first paint; next/script cannot be early enough |
| Vercel Web Analytics | <Analytics /> component | The package injects its own tag; wrapping it would fight it |
Three scripts, three mechanisms, and the reason is timing in two cases and ownership in the third.
The case next/script is for
// src/app/layout.tsx
{/* LemonSqueezy checkout overlay (docs.lemonsqueezy.com/help/lemonjs) —
detects clicks on .lemonsqueezy-button links; BuyButton also calls
window.createLemonSqueezy() itself once it renders such a link. */}
<Script
src="https://app.lemonsqueezy.com/js/lemon.js"
strategy="afterInteractive"
/>
Two props and a comment. This is what next/script is good at, and it earns its place for reasons a plain <script src> would not give you:
It is deduplicated by src. The component can appear in more than one place in the tree — a layout and a page, or two pages during a client-side navigation — and the script is fetched and executed once. A raw tag rendered by a React component that mounts twice runs twice.
afterInteractive is the right default here, and it is a default for a reason. The overlay's job begins when someone clicks a buy button, which cannot happen before the page is interactive. Loading it earlier would compete with the bundle that makes the click possible in the first place.
It survives client-side navigation. Next tracks which scripts have loaded across route changes, so moving between product pages does not re-execute the overlay.
The comment above the tag is doing real work too: it records that BuyButton calls window.createLemonSqueezy() itself once it renders a .lemonsqueezy-button link. That is the coupling strategy does not express — the script scans the DOM on load, so anything rendered after it loads has to announce itself. If you ship a third-party script that binds to elements at load time, expect to call its initializer again from the component that renders those elements.
The case next/script is wrong for
The theme bootstrap in the same file is not a <Script>, and this is the exception worth understanding, because it is the one people get wrong:
<head>
{/* Apply persisted theme before first paint to avoid a flash */}
<script
dangerouslySetInnerHTML={{
__html: `try{if(localStorage.getItem('theme')==='dark'){document.documentElement.classList.add('dark')}}catch(e){}`,
}}
/>
</head>
The requirement is that .dark is on <html> before the browser paints anything. Miss that by one frame and every visitor with dark mode saved sees a white flash. next/script with the default afterInteractive runs after hydration, which is far too late; even beforeInteractive — which does inject into the initial HTML — is documented for the root layout only and buys nothing over a plain tag here, while adding a component whose ordering guarantees are weaker than "it is literally the first thing in <head>".
So the rule that falls out of it: next/script manages loading; it cannot make anything earlier than the document itself. For render-blocking-by-design code — a theme class, a feature flag read, anything that must beat first paint — write the tag. Dark mode without the flash walks through the failure mode in detail, including why moving this script to <body> reintroduces it.
Note also the try/catch around a single localStorage.getItem. This code runs before anything else on the page, so an exception here is an exception with nothing to catch it — and localStorage throws outright in a browser with site data blocked. An inline bootstrap script has no error boundary above it; the try is the error boundary.
The third mechanism: scripts you do not own
import { Analytics } from "@vercel/analytics/next";
// ...
<Analytics />
<Analytics /> injects its own script tag and its own page-view tracking. There is no src to hand to <Script> and no reason to want one — the package decides its loading strategy, and wrapping a component that manages a script in a component that manages scripts gets you two managers.
The general form: if a vendor ships a React component, use the component. Reach for <Script> when the vendor ships a URL. Most of what people reach for <Script> for does not need it at all: a charting library is an npm import rendered by a Client Component, not a tag — how the dashboards here draw charts covers that path.
The part nobody mentions: every script is a CSP entry
This is the real cost of adding a <Script>, and it is invisible until you have a Content-Security-Policy. The script-src directive shipped by next.config.ts:
script-src 'self' 'unsafe-inline' https://app.lemonsqueezy.com https://assets.lemonsqueezy.com https://va.vercel-scripts.com
Every host in that list traces to exactly one thing in the census above, and the config file documents each one against the code that loads it:
* - script-src https://app.lemonsqueezy.com — the LemonSqueezy overlay
* `lemon.js`, loaded via <Script> in src/app/layout.tsx.
* - script-src https://va.vercel-scripts.com — Vercel Web Analytics
* (`@vercel/analytics` v2, mounted via <Analytics/> in src/app/layout.tsx).
Two consequences, and the second one is the expensive one.
A <Script src> pointing at a host you have not allowlisted is silently blocked. The element renders, nothing executes, and the only evidence is a console violation. If a third-party integration "does nothing" in production and works locally, check script-src before you check strategy.
The inline theme script is why this policy carries 'unsafe-inline'. The config says so outright:
* `'unsafe-inline'` on script-src/style-src is a DELIBERATE, documented
* tradeoff: Next.js injects an inline runtime <script>, this app has an inline
* theme-bootstrap <script> (layout.tsx), and Tailwind emits inline <style>. A
* nonce-based strict CSP would force every route to render dynamically per
* request and break the static (SSG) marketing pages.
That is the trade in full: an inline script needs either 'unsafe-inline' or a per-request nonce, a nonce cannot be baked into a statically prerendered page, and this site prerenders its marketing routes on purpose. Choosing the flash-free theme bootstrap therefore costs 'unsafe-inline' on script-src — a real weakening, accepted knowingly, and written down where the next person will find it. The full CSP and what each directive blocks covers the rest of the policy.
One smaller detail from the same file: 'unsafe-eval' is appended only when NODE_ENV === "development", because Turbopack's Fast Refresh evaluates modules via eval and production does not. A dev-only relaxation is fine; the mistake is shipping it.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Script tag is in the DOM but never runs | Its host is not in script-src; CSP blocked it | Add the exact origin to the policy, or drop the script |
| Third-party script runs but finds none of your elements | It bound to the DOM at load time, before your component rendered | Call the vendor's re-init function from that component, as BuyButton does |
| Flash of the wrong theme on first paint | Theme logic moved into <Script>, or into <body> | Inline <script> in <head>, before any markup |
| Script executes twice | A raw tag inside a component that mounts more than once | Use <Script>, which dedupes by src |
next/script inline usage throws about a missing id | Inline <Script> needs an explicit id to be deduped | Add id, or use a plain <script> if it must be inline anyway |
| Everything works locally, breaks on deploy | 'unsafe-eval' or a dev-only host was carrying it in development | Diff the dev and production policies; the dev allowance is not shipped |
Frequently asked questions
Which strategy should I use?
afterInteractive unless you can name why not. Use beforeInteractive only for scripts that must run before hydration — a consent manager or a bot detector — and only in the root layout, since that is the only place it is supported. lazyOnload suits anything purely decorative that can wait for idle time, like a chat widget.
Can I use next/script in a Server Component?
Yes. <Script> renders from the server fine; it needs no "use client" boundary, and the layout above is a Server Component. What it cannot do is run before the document paints.
Is a plain <script> tag ever the right answer?
Yes, for exactly the case above: code that must execute before first paint, kept inline in <head>. The cost is that inline scripts and strict CSP are fundamentally at odds, so budget for 'unsafe-inline' or a nonce, and decide which you can afford.
Do I need <Script> for a vendor that ships a React component?
No. Use the component. <Script> is for a URL, not for wrapping another integration.
Templates in this post
ASoc Quill is a lightweight blog template for makers, with a featured post, story feed and sidebar. ASoc Rally is an AI CRM marketing site with a pipeline preview, feature blocks and a three-tier pricing table. ASoc Rank is an SEO-audit SaaS site covering capabilities, pricing, done-for-you services and an FAQ. All three ship the layout and theme toggle already wired, so adding a third-party script is one tag and one CSP line rather than an audit.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
