css
css copied to clipboard
🐞 Remix Streaming SSR
What happened?
StackBlitz: https://stackblitz.com/github/itsMapleLeaf/mastercss-remix-test?file=app%2Fentry.client.tsx,app%2Froot.tsx,app%2Froutes%2Findex.tsx
Before hydration, it briefly shows the styles I would expect (I think?), but after hydration, they all go away, and don't get updated. I think this is unexpected?
Version
v2.0.0-beta.105
Relevant log output
No response
What browsers are you seeing the problem on?
No response
@itsMapleLeaf It's possible that the update of Remix in the past few months has caused the Master CSS JIT to not work. We've built a Remix example in CSS and will make a working version available asap. BTW, https://beta.css.master.co/docs is coming soon.
@itsMapleLeaf We're facing a known issue that cannot be fixed.
Just import MasterCSS into entry.client.tsx
import { RemixBrowser } from '@remix-run/react'
import { startTransition, StrictMode } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { MasterCSS } from '@master/css/dist'
function hydrate() {
startTransition(() => {
new MasterCSS()
hydrateRoot(
document,
<StrictMode>
<RemixBrowser />
</StrictMode>
)
})
}
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(hydrate)
} else {
// Safari doesn't support requestIdleCallback
// https://caniuse.com/requestidlecallback
setTimeout(hydrate, 1)
}
and got the error:
Uncaught Error: Hydration failed because the initial UI does not match what was rendered on the server.
at throwOnHydrationMismatch (entry.client-U5T4FRNQ.js:9132:17)
at tryToClaimNextHydratableInstance (entry.client-U5T4FRNQ.js:9153:15)
at updateHostComponent (entry.client-U5T4FRNQ.js:14365:13)
at beginWork (entry.client-U5T4FRNQ.js:15471:22)
at beginWork$1 (entry.client-U5T4FRNQ.js:19251:22)
at performUnitOfWork (entry.client-U5T4FRNQ.js:18696:20)
at workLoopConcurrent (entry.client-U5T4FRNQ.js:18687:13)
at renderRootConcurrent (entry.client-U5T4FRNQ.js:18662:15)
at performConcurrentWorkOnRoot (entry.client-U5T4FRNQ.js:18178:46)
at workLoop (entry.client-U5T4FRNQ.js:200:42)
Master CSS's JIT is just a simple JavaScript package; not sure how Remix works with React hydration.
I just simply customized some scripts like console.log('test')
in root.tsx and still got the same error.
Except for Remix, Master CSS's JIT currently works well with other frameworks.
Hm, I think the hydration issue happens because MasterCSS adds a
import { MasterCSS } from "@master/css"
import { useEffect, useLayoutEffect } from "react"
const useIsomorphicEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect
let initialized = false
export function useMasterCSS() {
useIsomorphicEffect(() => {
if (initialized) return
initialized = true
new MasterCSS()
})
}
import { useMasterCSS } from "./helpers/master-css"
export default function App() {
useMasterCSS()
// ...
}
So this is where hybrid rendering would come in. But I'm not sure how to set up hybrid rendering with streaming SSR 😅 I looked at the NextJS example, but it looks like MCSS expects all of the HTML to be available to generate the styles
Hm, I think the hydration issue happens because MasterCSS adds a
import { MasterCSS } from "@master/css" import { useEffect, useLayoutEffect } from "react" const useIsomorphicEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect let initialized = false export function useMasterCSS() { useIsomorphicEffect(() => { if (initialized) return initialized = true new MasterCSS() }) }
import { useMasterCSS } from "./helpers/master-css" export default function App() { useMasterCSS() // ... }
So this is where hybrid rendering would come in. But I'm not sure how to set up hybrid rendering with streaming SSR 😅 I looked at the NextJS example, but it looks like MCSS expects all of the HTML to be available to generate the styles
You are a professional! Hybrid Rendering ( AOT + JIT ) and Progressive Rendering ( SSR + JIT ) of Master CSS v2.0 are designed to avoid FOUC for better SEO and loading speed.
The way to practice SSR is very simple, but it must rely on the framework's transformHTML
Hook. This is an API built into Master CSS:
import { renderIntoHTML } from '@master/css'
import { config } from './master.css'
const html: string = `
<html>
<head></head>
<body>
<h1 class="text:center font:32">Hello World</h1>
</body>
</html>
`
const result = renderIntoHTML(html, config)
The official Remix guide isn't finished yet, and your attempts hastened its arrival.
Good to know you're taking a closer look at this!
Here's how Remix's default SSR setup works, it uses renderToPipeableStream
from React. Similarly, non-node templates use renderToStream
which returns a native ReadableStream
object.
view code
function handleBrowserRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
) {
return new Promise((resolve, reject) => {
let didError = false
const { pipe, abort } = renderToPipeableStream(
<RemixServer context={remixContext} url={request.url} />,
{
onShellReady() {
const body = new PassThrough()
responseHeaders.set("Content-Type", "text/html")
resolve(
new Response(body, {
headers: responseHeaders,
status: didError ? 500 : responseStatusCode,
}),
)
pipe(body)
},
onShellError(err: unknown) {
reject(err)
},
onError(error: unknown) {
didError = true
console.error(error)
},
},
)
setTimeout(abort, ABORT_DELAY)
})
}
From what I can tell, the problem with using renderIntoHTML
is it needs all of the HTML server-side up front without streaming, meaning we can't use features like defer
.
Does MasterCSS have an API to support SSR with streaming? I think not using defer
is a big compromise 😅
@itsMapleLeaf The host of Master CSS is basically only the root document
and the .shadowRoot
of Shadow DOM, and <style title="master">
is used as a communication bridge between SSR/AOT and JIT.
Therefore, all streaming of Remix after the document may need to go through JIT, and there is currently no CSS architecture that can collect scans across streams and merge them into the current page.
We need to provide a .hydrate()
to support SSR with streaming.
The SSG stage of Master CSS progressive rendering is mainly used to pre-generate the critical CSS rules required to improve FOUC, FCP, and CLS, also speed up page loading.
Any class names generated by client-side dynamic programming should be hydrated by the Master CSS JIT.

Is there any reason to pre-generate CSS rules on the server side during the streaming phase after the document is loaded?
Good to know you're taking a closer look at this!
Here's how Remix's default SSR setup works, it uses
renderToPipeableStream
from React. Similarly, non-node templates userenderToStream
which returns a nativeReadableStream
object.view code From what I can tell, the problem with using
renderIntoHTML
is it needs all of the HTML server-side up front without streaming, meaning we can't use features likedefer
.Does MasterCSS have an API to support SSR with streaming? I think not using
defer
is a big compromise 😅
We have to know how Remix reinserts CSS text into the document in case of SSR streaming.

That makes sense 🤔 The best way I can think of to insert the text into the streamed content is with react context
// entry.client.tsx
renderToPipeableStream(
<CssContext.Provider value={cssText}>
<RemixServer />
</CssContext.Provider>
)
// root.tsx
export default function App() {
const css = useContext(CssContext)
return (
<html>
<head>
<style dangerouslySetInnerHTML={{ __html: css }} />
</head>
{/* etc. */}
</html>
)
}
Also FWIW, a similar library named Twind has a built-in API for this: https://github.com/tw-in-js/twind/blob/main/examples/with-remix_react-v18/app/entry.server.tsx#L26
That makes sense 🤔 The best way I can think of to insert the text into the streamed content is with react context
// entry.client.tsx renderToPipeableStream( <CssContext.Provider value={cssText}> <RemixServer /> </CssContext.Provider> )
// root.tsx export default function App() { const css = useContext(CssContext) return ( <html> <head> <style dangerouslySetInnerHTML={{ __html: css }} /> </head> {/* etc. */} </html> ) }
Also FWIW, a similar library named Twind has a built-in API for this: https://github.com/tw-in-js/twind/blob/main/examples/with-remix_react-v18/app/entry.server.tsx#L26
I think we have come to a conclusion, we will try to design related API in @master/css.react
.
Thanks for your enthusiasm! This is very helpful!