Link to the code that reproduces this issue
https://github.com/iamibi/next.js/tree/repro/server-inserted-html-skip
To Reproduce
The registry is mounted only on /with-registry. Its callback returns a style on the first flush and an empty Fragment on subsequent flushes; both routes otherwise render the same delayed Suspense boundaries. The key callback is:
// app/style-registry.tsx (excerpt)
useServerInsertedHTML(() => {
const styles = registry.styles()
registry.flush()
return <>{styles}</>
})
The route streams one delayed Suspense boundary per requested ?boundaries=N, which causes multiple HTML and inline Flight chunks:
// app/streaming-content.tsx (excerpt)
import { Suspense } from 'react'
async function Delayed({ ms }: { ms: number }) {
await new Promise((resolve) => setTimeout(resolve, ms))
return <p>Resolved after {ms}ms</p>
}
export function StreamingContent({ boundaries }: { boundaries: number }) {
return (
<main>
{Array.from({ length: boundaries }, (_, index) => {
const ms = (index + 1) * 100
return (
<Suspense key={ms} fallback={<p>Loading ({ms}ms)...</p>}>
<Delayed ms={ms} />
</Suspense>
)
})}
</main>
)
}
The public reproduction is a minimal App Router app with a CSS-in-JS style registry and a control route without one. Its instrumentation.ts loads span-counter.ts, which counts Next.js OpenTelemetry spans per request. server_inserted_html_renders is AppRender.renderToReadableStream minus the page's own render. The counter is in the linked reproduction so the output below can be obtained without changing Next.js source.
git clone --branch repro/server-inserted-html-skip --single-branch https://github.com/iamibi/next.js.git
cd next.js
npm install
npx next info
npx next build
NEXT_OTEL_VERBOSE=1 npx next start
# In a second terminal:
curl -s -o /dev/null 'http://localhost:3000/without-registry?boundaries=5'
curl -s -o /dev/null 'http://localhost:3000/with-registry?boundaries=5'
curl -s -o /dev/null 'http://localhost:3000/with-registry?boundaries=20'
Fresh next start output on next@16.4.0-canary.50 (Turbopack), captured on 2026-09-27; each line is printed when its response finishes streaming:
[span-count] target=/without-registry?boundaries=5 fizz_renders=2 page_renders=1 server_inserted_html_renders=1
[span-count] target=/with-registry?boundaries=5 fizz_renders=15 page_renders=1 server_inserted_html_renders=14
[span-count] target=/with-registry?boundaries=20 fizz_renders=45 page_renders=1 server_inserted_html_renders=44
The HTML still contains the registry style exactly once. The recorded check was:
curl -s 'http://localhost:3000/with-registry?boundaries=5' | grep -o '<style>[^<]*</style>'
# <style>body{font-family:system-ui,sans-serif}</style>
### Current vs. Expected behavior
On `next@16.4.0-canary.50`, the route without a registry produced one server-inserted HTML render per request, while the registry route produced 14 with five boundaries and 44 with 20 boundaries. The same counts were previously observed on canary.48 and stable `16.3.6`. After the first flush, the additional renders produced empty strings; the response still contained the registry's style exactly once.
When every registered callback returns a node known to render no HTML and no other insertion remains, the server should skip the React render for that flush. Each registry callback must still run once and in order, and the response HTML and first style insertion should stay the same. With the local fix's filter applied to a canary runtime, the registry route performed one render for all tested boundary counts and produced byte-identical HTML.
### Provide environment information
```bash
Operating System:
Platform: darwin
Arch: arm64
Version: Darwin Kernel Version 25.6.0: Tue Aug 18 17:51:33 PDT 2026; root:xnu-12377.161.15.700.19~2/RELEASE_ARM64_T8132
Available memory (MB): 24576
Available CPU cores: 10
Binaries:
Node: 24.19.0
npm: N/A
Yarn: N/A
pnpm: 11.19.0
Relevant Packages:
next: 16.4.0-canary.50
eslint-config-next: N/A
react: 19.3.0
react-dom: 19.3.0
typescript: 5.9.3
Next.js Config:
output: N/A
Which area(s) are affected? (Select all that apply)
Performance, Loading UI and Streaming, CSS
Which stage(s) are affected? (Select all that apply)
next start (local)
Additional context
renderServerInsertedHTML() wraps every callback result in a keyed Fragment, even when the callback returns null, [], or an empty Fragment. That makes the result array nonempty and prevents getServerInsertedHTML() from taking its existing empty-result fast path. The skipped work includes a Fizz render and, for Node streams, an extra event-loop turn. A proposed fix that filters only nodes statically known to render nothing is in the linked pull request. This follows the skip work in #64408 and #85394. Other stages use related code paths but were not measured by this reproduction.
Link to the code that reproduces this issue
https://github.com/iamibi/next.js/tree/repro/server-inserted-html-skip
To Reproduce
The registry is mounted only on
/with-registry. Its callback returns a style on the first flush and an empty Fragment on subsequent flushes; both routes otherwise render the same delayed Suspense boundaries. The key callback is:The route streams one delayed Suspense boundary per requested
?boundaries=N, which causes multiple HTML and inline Flight chunks:The public reproduction is a minimal App Router app with a CSS-in-JS style registry and a control route without one. Its
instrumentation.tsloadsspan-counter.ts, which counts Next.js OpenTelemetry spans per request.server_inserted_html_rendersisAppRender.renderToReadableStreamminus the page's own render. The counter is in the linked reproduction so the output below can be obtained without changing Next.js source.git clone --branch repro/server-inserted-html-skip --single-branch https://github.com/iamibi/next.js.git cd next.js npm install npx next info npx next build NEXT_OTEL_VERBOSE=1 npx next startFresh
next startoutput onnext@16.4.0-canary.50(Turbopack), captured on 2026-09-27; each line is printed when its response finishes streaming:The HTML still contains the registry style exactly once. The recorded check was:
Which area(s) are affected? (Select all that apply)
Performance, Loading UI and Streaming, CSS
Which stage(s) are affected? (Select all that apply)
next start (local)
Additional context
renderServerInsertedHTML()wraps every callback result in a keyed Fragment, even when the callback returnsnull,[], or an empty Fragment. That makes the result array nonempty and preventsgetServerInsertedHTML()from taking its existing empty-result fast path. The skipped work includes a Fizz render and, for Node streams, an extra event-loop turn. A proposed fix that filters only nodes statically known to render nothing is in the linked pull request. This follows the skip work in #64408 and #85394. Other stages use related code paths but were not measured by this reproduction.