multiple Next.js Script components

6 min read

Next.js <Script /> load order can become inconsistent when multiple scripts share the same loading phase, causing dependencies to execute before their prerequisites and breaking production behavior in ways that are hard to reproduce locally.

Problem Overview

The issue reported in the reproduction repository shows that multiple Next.js <Script /> components do not always resolve in a predictable order. This becomes a real problem when one script depends on another, such as:

  • a vendor library that must load before an initialization script,
  • a global SDK that must exist before custom code runs,
  • or a polyfill that must execute before application logic.

At first glance, the JSX order suggests deterministic behavior. However, with Next.js script optimization and browser-level fetching, execution order is not guaranteed unless you explicitly design for dependency sequencing.

Understanding the Root Cause

The core reason is that Next.js <Script /> is not just a direct wrapper around a plain HTML <script> tag. It adds loading strategies, deduplication behavior, hydration-aware insertion, and performance optimizations. Those features are useful, but they also mean the final browser execution order can differ from the component render order.

Here is why this happens technically:

  • Same-strategy scripts are often fetched independently. If two scripts use the same strategy such as afterInteractive, they may start loading in parallel.
  • Network timing affects completion order. Even if Script A appears before Script B in JSX, Script B may finish downloading first.
  • Execution may follow availability, not author intent. If the framework injects scripts asynchronously, the browser can execute whichever script becomes ready first unless sequencing is enforced.
  • Hydration and client-side insertion add another layer. Some scripts are inserted after React hydration begins, so runtime conditions can influence when they attach and execute.

In short, component order is not the same as dependency order. If your implementation assumes that multiple <Script /> elements will always execute top-to-bottom, the assumption is unsafe.

This is especially visible when:

  • using external CDNs,
  • mixing inline and external scripts,
  • loading scripts in nested layouts or pages,
  • or relying on globally defined browser variables like window.SomeLibrary.

Step-by-Step Solution

The safest fix is to stop relying on implicit ordering and instead enforce explicit dependency sequencing.

1. Identify script dependencies

List every script that depends on another script. For example:

  • library.js must load first
  • plugin.js depends on library.js
  • init.js depends on both

2. Avoid separate parallel <Script /> tags for dependent assets

If scripts are coupled, rendering them as sibling <Script /> components with the same strategy can produce race conditions.

Problematic pattern:

<Script src="/scripts/library.js" strategy="afterInteractive" />
<Script src="/scripts/plugin.js" strategy="afterInteractive" />
<Script src="/scripts/init.js" strategy="afterInteractive" />

This looks ordered, but it does not guarantee execution order.

3. Chain loading with onLoad or combine files

If you must preserve order, use one of these approaches.

Option A: Load sequentially with state

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

export default function Page() {
  const [libraryReady, setLibraryReady] = useState(false)
  const [pluginReady, setPluginReady] = useState(false)

  return (
    <>
      <Script
        src="/scripts/library.js"
        strategy="afterInteractive"
        onLoad={() => setLibraryReady(true)}
      />

      {libraryReady && (
        <Script
          src="/scripts/plugin.js"
          strategy="afterInteractive"
          onLoad={() => setPluginReady(true)}
        />
      )}

      {pluginReady && (
        <Script
          id="init-script"
          strategy="afterInteractive"
        >
          {`
            if (window.Library && window.LibraryPlugin) {
              window.LibraryPlugin.init()
            }
          `}
        </Script>
      )}
    </>
  )
}

This pattern makes the dependency graph explicit. Script B does not render until Script A has finished loading.

Option B: Bundle dependent scripts together

If the files are tightly coupled and always needed together, the cleanest solution is often to combine them during your build process.

// next.config.js or your bundling pipeline
// Bundle library + plugin + init into one asset
// Then load a single deterministic file
<Script src="/scripts/combined-vendor-bundle.js" strategy="afterInteractive" />

This eliminates browser race conditions between related files.

Option C: Use plain <script> only when strict parser order is required

If a third-party integration absolutely requires classic document order semantics, a regular script tag in the appropriate document-level location may be more reliable than multiple optimized <Script /> components.

<script src="/scripts/library.js"></script>
<script src="/scripts/plugin.js"></script>
<script>
  window.LibraryPlugin.init()
</script>

Use this carefully, because you give up some of Next.js’s performance optimizations and script management benefits.

4. Choose the right strategy

Next.js provides several script strategies, and using the wrong one can amplify ordering issues:

  • beforeInteractive: for scripts that must load before the page becomes interactive.
  • afterInteractive: for scripts needed early, but not before initial hydration.
  • lazyOnload: for low-priority scripts that can wait until the browser is idle.

If a dependency must exist before any client code runs, beforeInteractive may be appropriate. But if you have multiple dependent scripts, strategy selection alone is still not enough; you must control sequencing.

5. Guard access to globals

Never assume a global object exists. Add runtime checks before using it.

if (typeof window !== 'undefined' && window.Library) {
  window.Library.doSomething()
}

This does not solve ordering by itself, but it prevents hard crashes and improves debuggability.

6. Verify behavior in production mode

Many script ordering bugs are harder to see in development because timing differs. Always test with a production build:

npm run build
npm run start

Then reproduce the issue using the steps from the repository’s README.

Common Edge Cases

  • Nested layouts or duplicate script declarations: The same script may be deduplicated or injected from multiple places, making actual runtime order harder to predict.
  • Inline init scripts running too early: An inline <Script /> can execute before a dependent external script finishes loading.
  • CDN latency differences: Even when two scripts are authored together, faster CDN responses can reverse the expected execution sequence.
  • App Router vs Pages Router differences: Placement and lifecycle behavior can vary depending on where scripts are declared.
  • Global variable collisions: Multiple third-party libraries may write to window, causing race conditions that look like ordering bugs.
  • Hydration mismatches: Client-only logic that assumes a script already ran may behave differently between server render and client hydration.

If you are integrating analytics, payment SDKs, ads, consent managers, or legacy browser plugins, these edge cases become much more likely.

FAQ

Does JSX order guarantee <Script /> execution order in Next.js?

No. JSX order reflects render intent, but not guaranteed browser execution order for multiple optimized scripts using the same loading strategy.

Should I always replace <Script /> with plain <script> tags?

No. Use Next.js <Script /> by default for optimization and framework integration. Switch to plain script tags only when a third-party dependency strictly requires classic sequential execution semantics.

What is the most reliable fix for dependent third-party scripts?

The most reliable fix is to explicitly sequence loading using onLoad, conditional rendering, or a single bundled file. Do not depend on sibling <Script /> order alone.

Bottom line: this bug is not usually about one script being broken. It is about assuming that framework-managed asynchronous script loading preserves dependency order. In Next.js, if execution order matters, make that order explicit.

Leave a Reply

Your email address will not be published. Required fields are marked *