Files
985394417f Fix styled-jsx race condition: styles lost due to concurrent rendering (#92459)
### What?

Fix a race condition in the Pages Router SSR path where styled-jsx
styles were dropped from the rendered HTML.

### Why?

`styledJsxInsertedHTML()` reads and flushes the styled-jsx style
registry. Previously it was called concurrently with the page render via
`Promise.all`:

```js
const [rawStyledJsxInsertedHTML, content] = await Promise.all([
  renderToString(styledJsxInsertedHTML()),
  (async () => { /* render the page */ })(),
])
```

Because both ran at the same time, `styledJsxInsertedHTML()` could (and
in practice did) execute and flush the registry **before** the page
render had finished populating it. The result was that dynamic
styled-jsx styles — those with interpolated expressions that compute
their class names at runtime via DJB2 hashing — were silently dropped
from the SSR output, causing a flash of unstyled content on first load.

This is particularly visible in production deployments where all
components use dynamic styled-jsx (numeric `jsx-*` class names), since
those styles only exist in the registry after rendering completes.

### How?

Serialize the two operations: render the page first, then call
`styledJsxInsertedHTML()`. Since the registry is fully populated by the
time it is read, all styles are captured correctly.

A new e2e test (`test/e2e/styled-jsx-dynamic`) exercises this scenario
with multiple nested components that all use dynamic styled-jsx with
interpolated props, covering the exact FOUC pattern seen in production.

<!-- NEXT_JS_LLM_PR -->

Co-authored-by: Tobias Koppers <[email protected]>
Co-authored-by: Claude <[email protected]>
2026-04-07 17:02:37 +02:00

28 lines
1.1 KiB
TypeScript

import { nextTestSetup } from 'e2e-utils'
describe('styled-jsx dynamic styles SSR', () => {
const { next } = nextTestSetup({
files: __dirname,
skipDeployment: true,
})
// Dynamic styled-jsx (with interpolated expressions) produces numeric class
// names at runtime via the DJB2 hash in styled-jsx's computeId function.
// This pattern matches production deployments where all jsx class names
// are numeric (e.g. jsx-2267428885) rather than hex (jsx-f36313d9f07883b7).
it('should contain dynamic styled-jsx styles during SSR', async () => {
const html = await next.render('/')
// Dynamic styled-jsx produces numeric class names at runtime
const numericClasses = html.match(/\bjsx-\d+\b/g) || []
console.log('Numeric jsx classes:', [...new Set(numericClasses)])
expect(numericClasses.length).toBeGreaterThan(0)
// All dynamic styles should be present as inline <style> tags
expect(html).toMatch(/color:.*?green/) // main page
expect(html).toMatch(/color:.*?blue/) // DynamicStyled
expect(html).toMatch(/background-color:.*?navy/) // header
expect(html).toMatch(/color:.*?purple/) // footer
})
})