fix(pages): preserve styled-jsx CSS ordering in SSR - #2894
Conversation
|
/bigbonk review for issues |
commit: |
|
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
Review:
|
|
Review posted to PR #2894. Summary of my findings: Review SummaryThe PR is well-implemented and thoroughly tested. No blocking issues found. I verified locally that all relevant tests pass (13 styled-jsx, 137 pages-response/data, 5 pages-router streaming/ISR) and Verified correct
Non-blocking observations (optional clarifying comments)
|
|
/bigbonk review for issues |
Review:
|
|
Review posted to PR #2894. Summary of findings: Review SummaryThorough review of the full diff, dev/prod parity across all four request-handling paths, and verification of the styled-jsx runtime shape against pinned Key findings
Non-blocking observations (optional clarifying comments)
|
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(pages): preserve styled-jsx during streaming SSR
Independent pass over the full diff, with source-level verification against the pinned styled-jsx@5.1.6 runtime and dev/prod parity across renderPagesPageResponse, streamPageToResponse, _renderIsrPassToStringAsync, and the ISR regeneration path. Ran the relevant suites locally — all green (14 styled-jsx, 137 pages-data/response) — and vp check is clean on all six changed source files. No blocking issues found.
Verified against the actual runtime
JSXStyleregisters styles synchronously during SSR render (styled-jsx/dist/index/index.js:472-474:if (typeof window === "undefined") { registry.add(props); return null; }). This is the load-bearing fact for the whole design: shell styles are all present whenrenderToReadableStreamresolves the shell, so the eagercollectPagesStyleRegistryHtml→<head>collection is complete, and Suspense/late styles register only as their boundaries render, so gating the second collection onbodyAllReadycaptures exactly those. No overlap.flush()clears_indices/_instancesCounts(:373-379), so the eager collect-then-flush cannot double-emit into the late collection. Confirmed.registry.styles()always returns an array (mapRulesToStyleuses.map(),:288-303), so both the prod entry'sstyles.length > 0guard andcollectPagesStyleRegistryHtml'sArray.isArray(styles) && styles.length === 0check are safe.STYLED_JSX_STYLE_PATTERNid prefix is correct:computeIdreturns"jsx-" + baseId(:260) andmapRulesToStyleprepends"__"(:294), yieldingid="__jsx-…", which the lookaheadid=["']__jsx-matches.convertStyledJsxRuntimeToEsmmatches the pinned runtime. The file tail is exactlyexports.StyleRegistry = …; exports.createStyleRegistry = …; exports.style = JSXStyle; exports.useStyleRegistry = useStyleRegistry;\n, so all three replacements hit; the fail-loudthrowon any no-op is a sound guard against future layout drift.
Verified correct
- ISR
tee()has no backpressure deadlock.bodyAllReadyis captured beforetee();finalShellSuffixawaitslateStyledJsxHTML → bodyAllReady. Both consumers drive the render independently —writePagesIsrCachedrains the cache branch viareadStreamAsTextandbuildPagesCompositeStreamdrains the response branch — and both await the samefinalShellSuffix. Neither waits on an undrained branch. The bot path bufferscompositeStream(which already awaitsfinalShellSuffix), so late styles land in the buffered/ETag'd HTML too. disableNextSsg: truefixes a real bug. Without it SWC's client-page tree-shaker rewritesgetStaticProps/getServerSidePropsto__N_SSG/__N_SSPand strips the functions from vinext's server module graph. The newstyled-jsx-static-props/styled-jsx-streamingfixtures and thenot.toContain("__N_SSP"/"__N_SSG")transform assertions lock this in.renderErrorPagerefactor is behavior-preserving. The removed non-Documentbranch is subsumed bystreamPageToResponse'selsedefault-shell path (dev-server.ts:513-529), andpreserveExistingContentType: statusCode === 404reproduces the old conditional Content-Type. Error pages additionally gain styled-jsx.- ISR head refresh is complete.
removeStyledJsxStylesis global/unanchored so it strips__jsx-styles from both<head>and the pre-</body>region on every regen (including when the fresh render emits none), then re-inserts the fresh run into<head>. Thedocument-isr-styledandisr-second-render-state(z-index: ${timestamp}) fixtures prove the styles actually advance and the stale ones are gone (not.toMatch(z-index: ${firstTimestamp})). - Fallback runtime is a coherent no-op. No
next→STYLED_JSX_FALLBACK_RUNTIME(empty registry,styles()→[]) andstyled-jsx/style→ null; the generated entry's static import never crashes and emits no styles.
Non-blocking observations (agree with prior review)
- Dev/prod encode different optionality for the styled-jsx import. Prod (
pages-server-entry.ts) statically importsstyled-jsx; dev (dev-server.ts:365) wraps it in try/catch. Both are safe because the plugin always resolves the id (real runtime or fallback), but a one-line comment on the prod static import stating why it is unconditionally safe would stop a future reader from "fixing" the asymmetry into an optional import (which would silently drop styles). STYLED_JSX_STYLE_PATTERNandconvertStyledJsxRuntimeToEsmare both coupled to the pinned runtime's shape (id-first attribute ordering / export tail). The ESM converter fails loud on drift; the style pattern does not. Acceptable given styled-jsx is pinned vianext.
Nothing here blocks merge.
|
Review posted to PR #2894. SummaryI performed an independent review of PR #2894 ( Key verifications (against runtime source)
Verified correct
Non-blocking (optional)
Tests pass locally (14 styled-jsx, 137 pages-data/response) and |
|
Independent exact-head review of I reviewed the full 13-file diff independently, including:
Fresh validation on this immutable head:
I found no actionable streaming/lifecycle gap, virtual-runtime or NUL-filter regression, dev/production/static-export divergence, or missing high-value test at this head. |
|
/bigbonk review for issues |
Review:
|
|
Review posted to PR #2894. SummaryI did an independent review of PR #2894 ( Verified correct
Non-blocking findings I raised (not covered by prior reviews)
Items 1 and 2 are the most valuable to address so the code and its tests stay honest about the streaming marker's current (non-)role. |
63e79a8 to
c423475
Compare
|
/bigbonk review for issues |
Review:
|
|
Review posted to PR #2894. SummaryI did an independent review of PR #2894 ( Key verifications
Non-blocking observations
All items are optional clarifying comments; nothing blocks merge. |
# Conflicts: # packages/vinext/src/server/dev-server.ts
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
@james-elicx Bonk workflow failed. Check the logs for details. View workflow run · To retry, trigger Bonk again. |
|
/bigbonk review for issues |
Review:
|
|
Review posted to PR #2894. Summary of findings: Review SummaryI did an independent review of PR #2894 ( Key verifications (including one my earlier suspicion cleared)
Non-blocking observations
All items are optional clarifying comments; nothing blocks merge. |
|
/bigbonk review for issues |
|
UnknownError: ProviderInitError |
|
/bigbonk review for issues |
There was a problem hiding this comment.
Review: fix(pages): preserve styled-jsx CSS ordering in SSR
Independent review at branch head a76fb59. I verified the styled-jsx collection lifecycle, the array-root render path, disableNextSsg safety against client leakage, the ISR head/style refresh, and dev/prod parity against the pinned styled-jsx@5.1.6 runtime source. Ran the relevant suites locally — all green (14 styled-jsx, 74 pages-page-response, 467 pages-data/router) — and tsc --noEmit is clean on the vinext package. No blocking issues found.
Verified correct
registry.styles()returns an array, and passing it as the render root is safe.mapRulesToStyle(styled-jsx/dist/index/index.js:288) returnscssRules.map(...), socollectPagesStyleRegistryHtmlhands an array straight torenderToReadableStream(styles)(no Fragment wrap, unlike the entry's ISR path). I confirmed empirically thatreact-dom/server.edge'srenderToReadableStreamaccepts an array root and renders it without error (<style>…</style><style>…</style>), so the unwrapped array is not a bug.disableNextSsg: truedoes not leak server code to the client. The styled-jsx transform runs across all environments, but client stripping is owned by the separatevinext:strip-server-exportsplugin (index.ts:5919, gated onthis.environment?.name === "client"+ pages-dir), which fires on any page whose code containsgetServerSideProps/getStaticProps. SWC's__N_SSG/__N_SSPtree-shaker was actively harmful here because it also ran over the server graph and made the data functions vanish at runtime. Real bug, correct fix; the newstyled-jsx-streaming.tsx(gSSP) +styled-jsx-static-props.tsxfixtures lock it in.allReadyrejection is handled before any headers or cache write.await bodyAllReadythrows (flushing the registry) before the ISRtee()/schedulePagesIsrCacheWriteand before anyResponseis constructed, so a failed render never persists partial HTML or emits headers. Same shape in dev'sstreamPageToResponse.- ISR
tee()has no undrained branch.bodyAllReadyis awaited beforetee();finalShellSuffixis an eagerly-resolvedPromise.resolve(...).bufferedFullHtmldrains branch 0,writePagesIsrCache/schedulePagesIsrCacheWritedrains branch 1, and both await the same already-resolved suffix. No deadlock. - The
__vinextStreamedHtmlResponsemarker is fully removed. No source or test references remain (prod-server.ts,worker-utils.ts,pages-request-pipeline.ts,unified-request-context.tsall cleaned; syntheticdeploy.test.ts/features.test.tscases deleted).renderPagesPageResponsenow deletes staleContent-Lengthand returns a bufferednew Response(string), which recomputes framing correctly. - ISR head/style refresh is placement-consistent and index-safe.
removeStyledJsxStylesstrips__jsx-styles from both<head>and pre-</body>on every regen (including empty fresh renders);refreshCachedHeadTagsrecomputes</head>on the already-stripped string via a length-preserving lowercase/blank-out, then re-injects the fresh run before</body>, matching the initial cache fill'sinjectPagesLateStylesplacement. Theisr-second-render-state.tsx(z-index: ${timestamp}) fixture proves styles advance. renderErrorPagerefactor is behavior-preserving. The removed non-Documentinline-HTML branch is subsumed bystreamPageToResponse's default-shell path; the newgetHeadHTMLconditionally seedscharset/viewportonly when Document is absent, andpreserveExistingContentType: statusCode === 404reproduces the old conditional Content-Type. Error pages additionally gain styled-jsx.
Non-blocking observations
-
injectPagesLateStyles/collectPagesStyleRegistryHtmlare byte-for-byte duplicated asinjectDevPagesLateStyles/collectDevPagesStyleRegistryHtmlindev-server.ts. AGENTS.md asks that response-shaping/streaming logic live in sharedserver/*helpers so dev and prod stay in lockstep. These four are pure functions with no dev/prod-specific behavior — a future edit to the</body>injection or the empty-styles guard has to be mirrored by hand across both files or they silently drift. Consider hoisting them into a shared module (e.g.server/pages-styled-jsx.ts) that bothpages-page-response.tsanddev-server.tsimport. -
Dev buffers the whole document but still sends
Transfer-Encoding: chunked(dev-server.ts:576, thenres.write(prefix)+res.end(bufferedBody + finalSuffix)). Since the body is fully known, dev could setContent-Lengthlike the prod/Worker path. Valid HTTP either way — a minor dev-only parity/efficiency nit. -
Dev/prod encode different optionality for the styled-jsx import. Prod (
pages-server-entry.ts:212) statically imports it; dev (dev-server.ts:352) wraps it in try/catch. Both are safe because the plugin always resolves the id (real runtime or theSTYLED_JSX_FALLBACK_RUNTIMEno-op), but a one-line comment on the prod static import stating why it is unconditionally safe would stop a future reader from "fixing" the asymmetry into an optional import (which would silently drop styles). (Raised in prior reviews; still applies.) -
removeStyledJsxStyles's<plaintext\b[^>]*>[\s\S]*$alternation consumes to end-of-string. If a<plaintext>ever preceded a styled-jsx<style>in cached HTML, that style would be swallowed and neither stripped nor refreshed on regen. React/Next never emit<plaintext>, so this is theoretical only — flagging for completeness alongside the (fail-loud)convertStyledJsxRuntimeToEsmruntime-shape coupling.
Nothing here blocks merge. Item 1 (the shared-helper dedup) is the most worth addressing per the repo's own entry/server layering guidance.
|
Review posted to PR #2894. Review SummaryI did an independent review of PR #2894 ( Key verifications
Non-blocking observations
Nothing blocks merge. |
Summary
allReadyin dev and production, matching Next.js and preventing later global CSS resources from overriding styled-jsx rulesContent-Lengthvalues before Worker response mergingPages Router behavior change
Pages Router Suspense HTML is intentionally no longer exposed progressively: body bytes wait for
renderStream.allReady. This trades Pages Router TTFB and progressive reveal for deterministic CSS ordering and Next.js parity. App Router streaming is unchanged, and PagesHEADrequests retain their headers-only fast path.Validation
vp checkon all changed source and test filestest/e2e/streaming-ssr/index.test.ts: 5/5 passedtest/e2e/app-dir/scss/with-styled-jsx/with-styled-jsx.test.ts: 2/2 passed across both Sass variantsReview
Two independent cumulative reviews returned no findings after the raw-text, Worker header, duplicate styled-jsx registry, behavior-contract, and dead HTML streaming-marker findings were addressed. The PR description was also replaced and verified from a literal body file so the documented commands and behavior remain intact.