Repository: next.js
Issue: Scripts from root layout don’t appear first in document head, including `beforeInteractive`
Labels: bug,Webpack,Navigation,Script (next/script)

### Link to the code that reproduces this issue

https://github.com/controversial/next-script-order-repro

### To Reproduce

1. `next build`
2. `next start`
3. Load localhost:3000

### Current vs. Expected behavior

Right now, scripts added to the root layout `<head>` appear in the rendered document *after* “chunk“ scripts from webpack output. Here’s how it looks in the attached reproduction—the server HTML contains five “next.js scripts” before the inline `inject-polyfill` script that was rendered at the beginning of the root `layout.tsx`’s head:

<img width="500" alt="Screenshot 2024-11-27 at 12 57 47 PM" src="https://github.com/user-attachments/assets/8c276139-a2bd-4d75-b09e-88cc4b4d17ce">

This behavior is misleading (or incorrect?) based on the docs—which say:
> `beforeInteractive`: Load the script **before any Next.js code** and before any page hydration occurs.

---

This ordering of scripts prevents the use case I showed in the reproduction: *conditionally injecting a polyfill that might be needed for client component rendering*.

```ts
if (/* features missing */) {
  const newScript = document.createElement('script');
  newScript.src = /* polyfill to load missing features */;
  document.head.prepend(newScript);
}
```

If such an inline script could appear *above* the webpack chunks, it could inject a polyfill whose load **blocks page hydration**, which is my desired behavior. Under the current system (where Next.js scripts are always initiated first) it’s a race condition between them and the polyfill—the Next.js have already started loading before we have a chance to perform feature detection and inject a blocking polyfill script above them.

---

Within the reproduction, currently, the console logs:
1. `injecting blocking polyfill script`
2. `client component mounted`
3. `polyfill script loaded, *after* component mounted`
    `Even though it was injected as a blocking script at the beginning of <head>, it didn’t get a chance to block loading Next.js chunks, because it wasn’t injected until *after* Next.js chunk scripts had already started loading`

I expect it to log:
1. `injecting blocking polyfill script`
2. `polyfill script loaded`
3. `client component mounted`

### Provide environment information

```bash
Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.2.0: Sun Nov  3 20:52:07 PST 2024; root:xnu-11215.60.405~54/RELEASE_ARM64_T6000
  Available memory (MB): 16384
  Available CPU cores: 10
Binaries:
  Node: 22.8.0
  npm: 10.8.2
  Yarn: 1.22.22
  pnpm: N/A
Relevant Packages:
  next: 15.0.4-canary.29
  eslint-config-next: N/A
  react: 19.0.0-rc-b01722d5-20241114
  react-dom: 19.0.0-rc-b01722d5-20241114
  typescript: 5.3.3
Next.js Config:
  output: N/A
```


### Which area(s) are affected? (Select all that apply)

Developer Experience, Navigation, Script (next/script), Webpack

### Which stage(s) are affected? (Select all that apply)

next build (local), next start (local), Vercel (Deployed)

### Additional context

I want to use an inline script for injecting polyfills because it avoids shipping the polyfill code to users that don’t need it.

I don’t want the polyfill code *itself* bundled as part of the root layout, because that increases the bundle size unnecessarily for *all users*. I’m okay with trading slightly slower load times on *outdated* browsers for better performance on recent browser versions.

Cloudflare’s polyfill service ships only the necessary features based on user-agent, but injecting the script after performing feature detection allows to avoid *even waiting for the server roundtrip* for users who would be receiving empty polyfills.

This problem affects *both* the “vanilla” `<script>` tag and the `<Script>` component from `next/script`