feat(core): warn in dev when SSR styles are rehydrated more than once#996
feat(core): warn in dev when SSR styles are rehydrated more than once#996vashek wants to merge 1 commit into
Conversation
renderToStyleElements returns the renderer's entire accumulated CSS on every call. In the Next.js App Router, useServerInsertedHTML runs once per streaming flush, so a setup that does not clear the renderer between flushes re-emits the full stylesheet and the copies are streamed into <body>. Those stale copies override styles inserted after a client-side navigation, silently breaking makeStyles overrides (they lose to their makeResetStyles base). The failure looks correct on a hard reload and only appears after a soft navigation, which makes it very hard to diagnose. rehydrateRendererCache scans every server-rendered <style> element, so it is the natural place to detect this: in development, count CSS rules that appear in more than one element (duplicate rule text, not merely a repeated bucket — a bucket legitimately recurs across flushes with different rules) and emit one console.error pointing at the renderer.stylesheets reset fix. Zero production overhead. Also DRYs the previously thrice-repeated devtools guard into a cacheRule helper.
|
@vashek please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.
Contributor License AgreementContribution License AgreementThis Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
|
|
@microsoft-github-policy-service agree |
| 'Clear the renderer after each flush:', | ||
| '\n\n', | ||
| 'useServerInsertedHTML(() => {\n const styles = renderToStyleElements(renderer);\n renderer.stylesheets = {};\n return styles;\n});', | ||
| '\n\n', |
There was a problem hiding this comment.
| 'Clear the renderer after each flush:', | |
| '\n\n', | |
| 'useServerInsertedHTML(() => {\n const styles = renderToStyleElements(renderer);\n renderer.stylesheets = {};\n return styles;\n});', | |
| '\n\n', |
Let's remove the actual recommendation from code.
| const seenRules: Set<string> | undefined = process.env.NODE_ENV !== 'production' ? new Set() : undefined; | ||
| let duplicateRuleCount = 0; | ||
|
|
||
| const cacheRule = (cssRule: string, bucketName: StyleBucketName) => { | ||
| if (seenRules) { | ||
| if (seenRules.has(cssRule)) { | ||
| duplicateRuleCount++; | ||
| } else { | ||
| seenRules.add(cssRule); | ||
| } | ||
| } |
There was a problem hiding this comment.
| const seenRules: Set<string> | undefined = process.env.NODE_ENV !== 'production' ? new Set() : undefined; | |
| let duplicateRuleCount = 0; | |
| const cacheRule = (cssRule: string, bucketName: StyleBucketName) => { | |
| if (seenRules) { | |
| if (seenRules.has(cssRule)) { | |
| duplicateRuleCount++; | |
| } else { | |
| seenRules.add(cssRule); | |
| } | |
| } | |
| const seenRules: Set<string> | undefined = new Set(); | |
| let duplicateRuleCount = 0; | |
| const cacheRule = (cssRule: string, bucketName: StyleBucketName) => { | |
| if (process.env.NODE_ENV !== 'production') { | |
| if (seenRules.has(cssRule)) { | |
| duplicateRuleCount++; | |
| } else { | |
| seenRules.add(cssRule); | |
| } | |
| } |
nit: let's write it this way. While Terser is able to DCE the original code, I would prefer to keep instructions gated in more solid way. Thx!
| @@ -0,0 +1,7 @@ | |||
| { | |||
| "type": "patch", | |||
| "comment": "Warn in development when duplicate CSS rules are found during rehydration — a signal that server-rendered styles were flushed into the HTML more than once (e.g. the Next.js App Router calling renderToStyleElements on every flush without clearing the renderer)", | |||
There was a problem hiding this comment.
| "comment": "Warn in development when duplicate CSS rules are found during rehydration — a signal that server-rendered styles were flushed into the HTML more than once (e.g. the Next.js App Router calling renderToStyleElements on every flush without clearing the renderer)", | |
| "comment": "chore: add development warnings for hydration issues in `rehydrateRendererCache`", |
|
@vashek Can you please check comments? Thx! |
renderToStyleElements returns the renderer's entire accumulated CSS on every call. In the Next.js App Router, useServerInsertedHTML runs once per streaming flush, so a setup that does not clear the renderer between flushes re-emits the full stylesheet and the copies are streamed into . Those stale copies override styles inserted after a client-side navigation, silently breaking makeStyles overrides (they lose to their makeResetStyles base). The failure looks correct on a hard reload and only appears after a soft navigation, which makes it very hard to diagnose.
rehydrateRendererCache scans every server-rendered <style> element, so it is the natural place to detect this: in development, count CSS rules that appear in more than one element (duplicate rule text, not merely a repeated bucket — a bucket legitimately recurs across flushes with different rules) and emit one console.error pointing at the renderer.stylesheets reset fix. Zero production overhead. Also DRYs the previously thrice-repeated devtools guard into a cacheRule helper.