# Fix the next/script hydration error

> A next/script hydration error happens when a script edits the server HTML before React hydrates. Load it afterInteractive or lazyOnload instead.

Source: https://hydration.jscrate.dev/docs/causes/third-party-script
Last updated: 2026-09-18

A next/script hydration error happens when a script changes the server-rendered
DOM before React hydrates. A tag manager, an A/B testing snippet, a consent
banner or a `beforeInteractive` script adds attributes, rewrites text or
inserts elements, and React finds HTML it did not render. Load the script after
hydration, or let it change only elements outside React's tree.

## Symptoms

```text
Hydration failed because the server rendered text didn't match the client.
Warning: Text content did not match. Server: "Hello world" Client: "Hola mundo"
Warning: Extra attributes from the server: data-experiment
Minified React error #418; visit https://react.dev/errors/418
```

The error appears only when the script wins the race against hydration, so it
often comes and goes with network speed and cache state.

hydration-proof reports it as [HP4001](https://hydration.jscrate.dev/docs/issues/hp4001), "The page was
modified before React hydrated", with the cause **A script changed the page
before hydration** and the exact change:

```text
HP4001 The page was modified before React hydrated  (a script changed the page before hydration, 80%)
  #greeting
  server: "Hello world"
  client: "Hola mundo"
  → Load the script after hydration (for example next/script with strategy="afterInteractive" or "lazyOnload"), or make it change only elements outside React's tree.
```

## Why the next/script hydration error happens

React hydrates the DOM as it finds it, not the HTML the server sent. Any script
that runs between the browser parsing the page and React hydrating it can
change what React sees:

- scripts in `<head>` without `defer` or `async`, and inline scripts
- `next/script` with `strategy="beforeInteractive"`, which Next.js injects into
  the `<head>` of the server HTML and fetches before any Next.js code
- tag managers and experimentation tools that swap text or classes as early
  as they can, to avoid a flash
- personalization, translation and consent tools that edit the page on load

### next/script beforeInteractive hydration error

The Next.js docs say `beforeInteractive` scripts are "downloaded before any
Next.js module", and that their execution "does not block page hydration from
occurring". A script that touches the DOM can therefore run before hydration or
during it. Scripts with this strategy in the root layout causing hydration
errors were reported in
[vercel/next.js#51242](https://github.com/vercel/next.js/issues/51242), now
closed. Keep `beforeInteractive` for scripts that must run first and leave the
DOM alone, such as bot detectors and consent managers.

### next/script nonce CSP hydration warning

With a Content Security Policy that uses a nonce, you may see:

```text
Warning: Extra attributes from the server: nonce
```

Browsers hide the `nonce` attribute from the DOM after parsing, for security,
so the element React hydrates no longer shows the value the server rendered
([react/react#26028](https://github.com/facebook/react/issues/26028)). The
script still runs with its nonce. Do not work around the warning by exposing
the nonce to client code, which weakens the policy. Let Next.js apply the nonce
on the server as its
[CSP guide](https://nextjs.org/docs/app/guides/content-security-policy)
describes, and keep Next.js up to date: the report in
[vercel/next.js#77952](https://github.com/vercel/next.js/issues/77952) was
closed as a duplicate of #63749.

### Vercel Speed Insights hydration error

A Vercel Speed Insights hydration error with minified errors 418 and 423 was
reported in [vercel/next.js#59379](https://github.com/vercel/next.js/issues/59379)
against a Next.js 14 canary, and is closed. If you still see it, upgrade
`@vercel/speed-insights` and Next.js, then run hydration-proof: it shows
whether the page was changed before hydration (HP4001) or has an ordinary
mismatch with another cause.

## How to fix it

### Load the script after hydration

`afterInteractive` (the default) injects the script on the client after some
hydration has happened. `lazyOnload` waits for browser idle time:

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {children}
        {/* Before: strategy="beforeInteractive" */}
        <Script
          src="https://example.com/experiments.js"
          strategy="afterInteractive"
        />
        <Script
          src="https://example.com/chat-widget.js"
          strategy="lazyOnload"
        />
      </body>
    </html>
  );
}
```

Tag managers and analytics fit `afterInteractive`; chat widgets and social
embeds fit `lazyOnload`.

### Give the script its own element

If a script must run early, let it change only elements React does not render:
a container it creates itself and appends to `<body>`, or one your layout
renders empty and never updates. Changes outside React's roots cannot break
hydration, and hydration-proof reports them as info at most.

### Move experiments to the server

A/B tests that swap copy in the browser before hydration change React's DOM by
design. Decide the variant on the server (in middleware or the page) and render
it directly, so both sides agree. See
[server and client data](https://hydration.jscrate.dev/docs/causes/data).

### Mark inline theme scripts

An inline script that sets a class on `<html>` before paint (a theme script) is
fine if it changes only `<html>` and that element has
`suppressHydrationWarning`. The [theme guide](https://hydration.jscrate.dev/docs/causes/theme) shows the
pattern.

## Catch it with ESLint

No lint rule can see what an external script does at runtime, so there is no
rule for this cause. `hydration-proof test` catches it in the running page.

## Catch it in CI

hydration-proof compares the server HTML as the browser parsed it with the DOM
right before React hydrated, after undoing React's own streaming moves.
Whatever remains was changed by someone else: HP4001 for your scripts and
third-party tags, [HP4002](https://hydration.jscrate.dev/docs/issues/hp4002) for changes that look like a
browser extension. To test a tag before you ship it, add it to a scenario's
`initScripts`, which run before the page's own scripts. See
[how it works](https://hydration.jscrate.dev/docs/how-it-works).

```bash
npx hydration-proof test
```

## Related

- [Browser extensions that change the page](https://hydration.jscrate.dev/docs/causes/extension)
- [HP4001: the page was modified before React hydrated](https://hydration.jscrate.dev/docs/issues/hp4001)
- [Dark mode scripts and suppressHydrationWarning](https://hydration.jscrate.dev/docs/causes/theme)
- [Hydration errors in Next.js](https://hydration.jscrate.dev/docs/frameworks/nextjs)
- [Minified React error #418](https://hydration.jscrate.dev/docs/errors/minified-react-error-418)
