FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Issue · GitHub

/ next.js Public

Cross-site assetPrefix makes Chrome 139+ delay execution of every chunk script #97531

Description

Link to the code that reproduces this issue

https://github.com/flanker/nextjs-cross-site-chunk-delay

To Reproduce

The repro is deployed as two Vercel projects built from the same source with the same
ASSET_PREFIX, so they serve byte-identical HTML apart from the build ID. vercel.app is
on the Public Suffix List, so the two hosts are different sites, not merely different
origins. The only variable is the host in the address bar.

open document host chunk host arm
https://xsite-repro.vercel.app/ xsite-repro.vercel.app same same-site control
https://xsite-repro-doc.vercel.app/ xsite-repro-doc.vercel.app xsite-repro.vercel.app cross-site
  1. git clone https://github.com/flanker/nextjs-cross-site-chunk-delay && cd $_
  2. Run the harness against both arms (Node >= 22, no npm dependencies):
    node scripts/measure.mjs https://xsite-repro.vercel.app/ https://xsite-repro-doc.vercel.app/
  3. Run the control, which is what attributes the difference to Chrome rather than to
    anything about the two hosts:
    DISABLE_FEATURE=1 node scripts/measure.mjs https://xsite-repro.vercel.app/ https://xsite-repro-doc.vercel.app/

React's scheduler yields explode only when the chunks are cross-site and the Chrome
feature is on; disabling the feature collapses the difference (numbers under "Additional
context", including an honest note on what this repro does not show).

Current vs. Expected behavior

Current. Chrome 139 enabled the Blink feature kLowPriorityAsyncScriptExecution by
default. It delays execution of cross-site classic async scripts. App Router emits its
chunks as cross-site classic async scripts whenever assetPrefix points at a CDN on a
different registrable domain — the setup the assetPrefix docs describe. React hydration
then stalls waiting on chunks the browser has decided are not urgent. There is no warning
and no error; the only symptom is main-thread script time.

Defaults in third_party/blink/common/features.cc:

parameter default effect
cross_site_only true only cross-site scripts are delayed
main_frame_only true only the main frame
exclude_non_parser_inserted false dynamically inserted scripts are delayed too
opt_out_high_fetch_priority_hint true fetchpriority="high" opts out

Expected. Next marks its own chunk scripts fetchpriority="high" so they opt out of the
delay. That last parameter is the escape hatch Chrome provides for exactly this case, and
ReactDOM.preinit already accepts fetchPriority in PreinitOptions —
required-scripts.tsx simply does not pass it.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.5.0
  Available memory (MB): 36864
  Available CPU cores: 12
Binaries:
  Node: 22.16.0
  npm: 10.9.2
  pnpm: 10.33.4
Relevant Packages:
  next: 16.2.6
  react: 19.2.1
  react-dom: 19.2.1
  typescript: 5.9.3
Next.js Config:
  output: standalone

Still present on canary. The measurements above are from 16.2.6, but the code path is
unchanged: packages/next/src/server/app-render/required-scripts.tsx is byte-identical
across v16.2.6, v16.3.1 and canary (md5 3d2d1d3e5be2f67b7060cd98ec4af7ce), and
fetchPriority does not appear in the file on any of them. Upgrading does not fix this.

Which area(s) are affected? (Select all that apply)

Performance, Runtime, Turbopack

Which stage(s) are affected? (Select all that apply)

next start (local), Vercel (Deployed), Other (Deployed)

Additional context

Where Next is exposed

App Router emits chunk scripts through three paths, and they do not all belong to Next:

path emitted by can Next set fetchPriority today?
rootMainFiles[1..] ReactDOM.preinit in required-scripts.tsx yes
rootMainFiles[0] React bootstrapScripts (the id="_R_" script) no — BootstrapScriptDescriptor has no such field
client component chunks React Flight client-reference manifest → preinitScriptForSSR no

Counted on one production App Router page with 78 chunk <script> tags in the SSR HTML:
10 from the first path, 1 from the second, 67 from the third. So a Next-only fix
is correct but partial; it is also the only part that can land without React API changes.

Controlled experiment on a production app

The same production HTML fulfilled under four document hosts via CDP Fetch.fulfillRequest,
every chunk served by the real CDN. Full request interception confirmed the arms were
byte-identical (55 CDN successes / 10 failures / 3,312,160 JS bytes, identical across arms)
and hydration succeeded in all four. Chrome 151.

document host relation to chunks ScriptDuration cold warm scheduler yields cold / warm
im.jsjform.com same-site 241 ms 51 ms 5,527 / 36
zz.jsjform.com same-site 260 ms 48 ms 6,710 / 23
im.jinshuju.net cross-site 2,997 ms 2,807 ms 109,876 / 98,487
im.jsjform-x.net cross-site, domain does not resolve 2,858 ms 2,802 ms 180,964 / 98,373

12–55×. The fourth arm uses a domain that does not exist, ruling out anything specific to
the third arm's host.

A warm cache does not help — it widens the gap (12× → 55×): same-site warm drops to
51 ms / 36 yields while cross-site barely moves.

Before / after in production

Marking the chunks fetchpriority="high" via a local patch (SSR path plus the Turbopack
client chunk loader), same account, machine and network:

LCP main-thread script
before 3.5–4 s ~2000 ms
Chrome with --disable-features=LowPriorityAsyncScriptExecution ~1.6 s ~700 ms
after the patch ~2 s ~700 ms

The residual gap between rows 2 and 3 is the bootstrapScript, which the patch cannot
reach.

What the linked repro does and does not show

It demonstrates the mechanism, not the magnitude. 300 client components, 7 chunk scripts,
9 runs per cell, median [min–max]:

ScriptDuration scheduler yields
feature on, same-site 21 ms [20–155] 7 [7–18352]
feature on, cross-site 23 ms [23–162] 308 [8–19335]
feature off, same-site 61 ms [60–75] 15 [15–1076]
feature off, cross-site 21 ms [20–78] 9 [7–1573]

The medians form the expected 2×2, but per-run ranges overlap heavily and ScriptDuration
shows no effect, so I would not lean on this alone — the production experiment above is the
controlled one. Two dead ends are recorded in the repro README: loopback hosts
(localhost vs 127.0.0.1) do not engage the feature at all, and raising the chunk count
via splitChunks.maxSize makes the effect disappear, because the pieces land in the entry
chunk group and are preinited before hydration starts. The trigger is chunks React must
wait on during hydration, not chunk count.

Fully closing this needs React

  1. fetchPriority accepted in BootstrapScriptDescriptor (preinit already accepts it)
  2. a way for a host framework to set fetchPriority on client-reference preinits

Happy to open a React issue for those, but wanted to check here first whether the Next team
would rather carry that conversation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    PerformanceAnything with regards to Next.js performance.RuntimeRelated to Node.js or Edge Runtime with Next.js.TurbopackRelated to Turbopack with Next.js.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions


    Back | FazBrowse Home | New Git URL