Skip to content

fix: serialize SSR render and head collection in Pages Router (#792)#951

Merged
james-elicx merged 1 commit intocloudflare:mainfrom
Divkix:fix/792-styled-jsx-race
Apr 29, 2026
Merged

fix: serialize SSR render and head collection in Pages Router (#792)#951
james-elicx merged 1 commit intocloudflare:mainfrom
Divkix:fix/792-styled-jsx-race

Conversation

@Divkix
Copy link
Copy Markdown
Contributor

@Divkix Divkix commented Apr 29, 2026

Fixes #792

Summary

  • Audited styled-jsx race condition: vinext does not call styledJsxInsertedHTML() (no styled-jsx style registry exists), so the specific Next.js bug does not apply
  • Fixed a related ordering bug in Pages Router prod path: getSSRHeadHTML() was called before renderToReadableStream(), causing <Head> content to be silently dropped from SSR HTML
  • Fixed clearSsrContext() ordering: SSR context was cleared before rendering, so useRouter() would return defaults during SSR
  • Verified dev-server.ts and app-ssr-entry.ts are already correct (render first, then collect styles)
  • Added ordering tests to verify render completes before style/context collection

Test plan

  • Added 2 unit tests in pages-page-response.test.ts that verify renderToReadableStream is called before getSSRHeadHTML and clearSsrContext
  • All existing pages-page-response tests (7 total) pass
  • All head tests (30) pass
  • pages-router.test.ts (196 tests) pass
  • Build, lint, format, typecheck all pass

…lare#792)

Move renderToReadableStream() before buildPagesShellHtml() + clearSsrContext()
so that <Head> and other SSR state collectors (styled-jsx, useServerInsertedHTML)
are populated before they are read. This fixes a race condition where head styles
were silently dropped because they were collected before the page had finished
rendering.

Mirrors Next.js fix: vercel/next.js@9853944

The specific styled-jsx race condition (styledJsxInsertedHTML() called
concurrently with page render via Promise.all) does not exist in vinext because
vinext does not implement a styled-jsx style registry at all. However, the same
class of ordering bug affected vinext's Pages Router prod path where getSSRHeadHTML
was called before rendering completed.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 29, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@951

commit: 46011bf

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is correct and important. Moving renderToReadableStream before buildPagesShellHtml ensures <Head> content is populated before getSSRHeadHTML() collects it. This brings pages-page-response.ts in line with dev-server.ts, which already had the correct ordering (render at line 117, collect head at line 120, clear context at line 978).

A couple of minor nits below, neither blocking.

// (e.g. styled-jsx, useServerInsertedHTML) are populated before we read
// them. This fixes a race condition where head styles were silently dropped
// because they were collected before the page had finished rendering.
// Mirrors Next.js fix: vercel/next.js@9853944
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the comment says "Mirrors Next.js fix: vercel/next.js@9853944" — this is a bare commit hash with no hyperlink, which makes it hard to look up. Consider making it a full URL:

Suggested change
// Mirrors Next.js fix: vercel/next.js@9853944
// Mirrors Next.js fix: https://github.com/vercel/next.js/commit/9853944

expect(callOrder).toEqual(["render", "head"]);
});

it("clears SSR context only after rendering, not before", async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the test name says "clears SSR context only after rendering, not before" — but in the old code, clearSsrContext was already called after buildPagesShellHtml (which itself was after getSSRHeadHTML). The actual bug was that renderToReadableStream came after both of those, not that clearSsrContext was called too early. The test is still valid as a regression guard for the new ordering, but the name slightly mischaracterizes the original bug. Something like "renders page before clearing SSR context" would be more precise.

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix is correct and important. Moving renderToReadableStream before buildPagesShellHtml ensures <Head> content is populated before getSSRHeadHTML() collects it. This brings pages-page-response.ts in line with dev-server.ts, which already had the correct ordering (render at line 117, collect head at line 120, clear context at line 978).

A couple of minor nits below, neither blocking.

// (e.g. styled-jsx, useServerInsertedHTML) are populated before we read
// them. This fixes a race condition where head styles were silently dropped
// because they were collected before the page had finished rendering.
// Mirrors Next.js fix: vercel/next.js@9853944
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the comment says "Mirrors Next.js fix: vercel/next.js@9853944" — this is a bare commit hash with no hyperlink, which makes it hard to look up. Consider making it a full URL:

Suggested change
// Mirrors Next.js fix: vercel/next.js@9853944
// Mirrors Next.js fix: https://github.com/vercel/next.js/commit/9853944

expect(callOrder).toEqual(["render", "head"]);
});

it("clears SSR context only after rendering, not before", async () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the test name says "clears SSR context only after rendering, not before" — but in the old code, clearSsrContext was already called after buildPagesShellHtml (which itself was after getSSRHeadHTML). The actual bug was that renderToReadableStream came after both of those, not that clearSsrContext was called too early. The test is still valid as a regression guard for the new ordering, but the name slightly mischaracterizes the original bug. Something like "renders page before clearing SSR context" would be more precise.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented Apr 29, 2026

Review submitted. Here's a summary:

Approved the PR with two minor nits:

  1. The fix is correct. The old code in pages-page-response.ts called getSSRHeadHTML() (inside buildPagesShellHtml) before renderToReadableStream, meaning <Head> tags from the page tree were never collected. Moving the render call earlier matches what dev-server.ts already does correctly.

  2. Nit on the comment: The vercel/next.js@9853944 reference would be more useful as a full GitHub URL for easy clickthrough.

  3. Nit on the second test name: "clears SSR context only after rendering, not before" slightly mischaracterizes the original bug — clearSsrContext was never called before rendering in the old code either. The real issue was that renderToReadableStream came after head collection. The test itself is valid as a regression guard though.

github run

@james-elicx james-elicx merged commit f4d4ad6 into cloudflare:main Apr 29, 2026
28 checks passed
@Divkix Divkix deleted the fix/792-styled-jsx-race branch April 29, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Audit Pages Router SSR for styled-jsx race condition (concurrent rendering drops dynamic styles)

2 participants