[PR #926] [MERGED] fix(app-router): flush server-inserted HTML during SSR streaming #954

Closed
opened 2026-05-06 13:11:13 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/926
Author: @NathanDrake2406
Created: 4/28/2026
Status: Merged
Merged: 4/28/2026
Merged by: @james-elicx

Base: mainHead: nathan/fix-server-inserted-html-streaming


📝 Commits (1)

  • abda7f4 fix(app-router): flush server-inserted HTML during SSR streaming

📊 Changes

6 files changed (+173 additions, -17 deletions)

View changed files

📝 packages/vinext/src/server/app-ssr-entry.ts (+28 -13)
📝 packages/vinext/src/server/app-ssr-stream.ts (+26 -4)
📝 packages/vinext/src/shims/navigation.react-server.ts (+1 -0)
📝 packages/vinext/src/shims/navigation.ts (+22 -0)
📝 tests/rsc-streaming.test.ts (+84 -0)
📝 tests/server-inserted-html.test.ts (+12 -0)

📄 Description

What this changes

App Router SSR now asks for useServerInsertedHTML output on each HTML streaming flush instead of only once before returning the stream. The first insertion still places the navigation/bootstrap/font/server HTML before </head>, while later insertions emit newly collected server-inserted HTML before the next streamed chunk.

Why

CSS-in-JS registries such as styled-components and Emotion register a callback during shell render, then collect additional styles as Suspense boundaries continue rendering. Vinext previously flushed and cleared those callbacks before the stream was consumed, so styles collected after the shell never reached streamed HTML.

Next.js keeps server-inserted HTML callbacks registered and asks for insertion HTML from its stream transform repeatedly:

Approach

  • Add renderServerInsertedHTML() for streaming SSR so callbacks can be invoked without unregistering them.
  • Keep callback cleanup deferred until the returned SSR stream is consumed or cancelled.
  • Let createTickBufferedTransform() accept an insertion function and call it after the initial head injection on each buffered flush.
  • Preserve the existing one-shot string insertion path for current non-streaming callers.

Validation

  • vp check
  • vp test run tests/server-inserted-html.test.ts tests/server-inserted-html-context.test.ts tests/rsc-streaming.test.ts tests/app-router.test.ts tests/features.test.ts tests/app-page-stream.test.ts

Risks / follow-ups

The change is scoped to App Router SSR streaming and the existing tick-buffered transform. The test coverage is unit and integration-level; it ports the Next.js streaming order assertion without adding a full styled-components dependency fixture to vinext.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/926 **Author:** [@NathanDrake2406](https://github.com/NathanDrake2406) **Created:** 4/28/2026 **Status:** ✅ Merged **Merged:** 4/28/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `nathan/fix-server-inserted-html-streaming` --- ### 📝 Commits (1) - [`abda7f4`](https://github.com/cloudflare/vinext/commit/abda7f4180139e58636380cf7b4a3daa3bad198a) fix(app-router): flush server-inserted HTML during SSR streaming ### 📊 Changes **6 files changed** (+173 additions, -17 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/server/app-ssr-entry.ts` (+28 -13) 📝 `packages/vinext/src/server/app-ssr-stream.ts` (+26 -4) 📝 `packages/vinext/src/shims/navigation.react-server.ts` (+1 -0) 📝 `packages/vinext/src/shims/navigation.ts` (+22 -0) 📝 `tests/rsc-streaming.test.ts` (+84 -0) 📝 `tests/server-inserted-html.test.ts` (+12 -0) </details> ### 📄 Description ## What this changes App Router SSR now asks for `useServerInsertedHTML` output on each HTML streaming flush instead of only once before returning the stream. The first insertion still places the navigation/bootstrap/font/server HTML before `</head>`, while later insertions emit newly collected server-inserted HTML before the next streamed chunk. ## Why CSS-in-JS registries such as styled-components and Emotion register a callback during shell render, then collect additional styles as Suspense boundaries continue rendering. Vinext previously flushed and cleared those callbacks before the stream was consumed, so styles collected after the shell never reached streamed HTML. Next.js keeps server-inserted HTML callbacks registered and asks for insertion HTML from its stream transform repeatedly: - [`createServerInsertedHTML` stores callbacks without clearing them](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/server/app-render/server-inserted-html.tsx#L9-L30) - [`makeGetServerInsertedHTML` renders the registered callbacks each time it is called](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/server/app-render/make-get-server-inserted-html.tsx#L38-L109) - [`createHeadInsertionTransformStream` invokes insertion during stream transforms and final flush](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/packages/next/src/server/stream-utils/node-web-streams-helper.ts#L447-L520) - [Next.js e2e coverage asserts Suspense CSS-in-JS styles stream before the refresh script](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts#L50-L68) - [The referenced fixture renders a styled-components node inside Suspense](https://github.com/vercel/next.js/blob/ae61573e062e900050b8e6b24626e450accc4570/test/e2e/app-dir/use-server-inserted-html/app/css-in-js/suspense/page.js#L31-L58) ## Approach - Add `renderServerInsertedHTML()` for streaming SSR so callbacks can be invoked without unregistering them. - Keep callback cleanup deferred until the returned SSR stream is consumed or cancelled. - Let `createTickBufferedTransform()` accept an insertion function and call it after the initial head injection on each buffered flush. - Preserve the existing one-shot string insertion path for current non-streaming callers. ## Validation - `vp check` - `vp test run tests/server-inserted-html.test.ts tests/server-inserted-html-context.test.ts tests/rsc-streaming.test.ts tests/app-router.test.ts tests/features.test.ts tests/app-page-stream.test.ts` ## Risks / follow-ups The change is scoped to App Router SSR streaming and the existing tick-buffered transform. The test coverage is unit and integration-level; it ports the Next.js streaming order assertion without adding a full styled-components dependency fixture to vinext. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:11:13 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#954
No description provided.