-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React 18 Streaming SSR #3658
Comments
Is there any plan to support this? React 18 has been released this month and I would like to be able to switch to it. |
any news regarding react 18 ssr support ? |
Hey @gshokanov, I'm curious to know whether your spike included using Styled Components in a delayed content block that is wrapped in Suspense? I was assuming that |
I came up with a pretty simple workaround, pipe renderToPipeableStream into a duplex stream, which you can then pass to interleaveWithNodeStream. You can see here: https://github.com/adbutterfield/fast-refresh-express/blob/react-18/server/renderToStream.ts Of course you don't get true streaming render though. Currently you get a hydration mismatch error, which I think is the same as this issue: facebook/react#24430 |
Next.js has now rolled out streaming SSR support so this is now a big blocker for folks to opt-in to that. https://nextjs.org/docs/advanced-features/react-18/streaming Here's the style upgrade guide for library authors |
Maybe not ideal, but I got something working now. Might be a way to go if you want to upgrade to React 18 now, and then hopefully get all the benefits of streaming rendering sometime in the future. |
This is a pretty big blocker for many. In our team, we are working to upgrade to React18 and one of our main bets, to solve some remaining TTFB Issues, would be to use HTML Streaming or however you wish to call it. Is this on the pipeline at all or not? I see the team is working actively on the beta v6, but I see no mentions at all. |
@freese the best that I can determine is that This hook (as I understand it) is specifically for authors of css-in-js libraries for inserting global DOM nodes (like I take this as a sign the authors are working towards a solution. Might not be fully realized until a Worth noting that React's official stance on this is:
|
Curious how this will be solved. How about keeping a buffer of styles while components are rendering and every time there's a chance to emit a |
I managed to emit a style tag for each boundary component, but it was only possible by changing the ReactDOMServer code to expose a hook. Since React has made it clear in reactwg/react-18#110 that they would not support anything new upstream to accommodate this kind of CSS-in-JS problem, my solution would be a hack at this point and maybe a risky thing to be used in production. Based on the same doc, it speculates that there will be performance implications from the concurrent mode of React 18, even if you could solve this streaming issue. |
Emotion does React 18 streaming by inserting styles in the stream https://github.com/emotion-js/emotion/blob/92be52d894c7d81d013285e9dfe90820e6b178f8/packages/react/src/emotion-element.js#L149-L153 |
Looks like this PR is doing something similar to this suggestion: #3821 Wondering if this gets us any closer to React18 SSR support. Huge blocker for us, so I'm interested to hear any contributor feedback on potential solutions. |
But it seems like Emotion doesn't support renderToPipeableStream either, otherwise it seems like emotion/styles might be a pretty simple drop-in replacement, the syntax looks identical to Styled Components. Hopefully one of these libraries is able to add support soon -- my massive React app is 50% CSS (most components have an equal amount of CSS vs JS/JSX), so the thought of migrating to something like CSS modules is keeping me up at night. |
@ericselkpc take a look at https://github.com/wmertens/styled-vanilla-extract - it's for qwik right now but adding react would not be hard. |
Thanks. We use a lot of props in our styled components that would be difficult to migrate to inline styles or other methods. Very nice work though. I love the zero runtime idea, just maybe not practical in our case where content comes from CMS and would require a new build on each change to have full SSG instead of SSR. |
I think this code would need to change: styled-components/packages/styled-components/src/models/ServerStyleSheet.tsx Lines 109 to 117 in c9cfa34
It seems like the chunks emitted by ReactPartialRenderer are even more granular than they were in React 17 and below. |
Hi, it's March - and it seems that I got heavily occupied with other OSS work and couldn't find the time for this one. I need to keep my word though so I'll be dusting off my work on this over the weekend and I plan to wrap it up soon. I already have a working PoC + some extra pointers from the Next.js team but it's the last 20% of the work that takes the most time 😉 |
@Andarist since this issue is marked "help wanted"—is there anything I could do to help get this over the finish line? Alpha testing a forked version of the package? Code review? Pairing? Thanks for all the hard work you've clearly already done on this :) as you can probably deduce, I'm excited to get this into my NextJS app 🙂 |
Actually, never mind! Next seems to have already resolved streaming SSR issues with styled-components — @Andarist you perhaps alluded to that here?
|
o really?, and what is the solution and where is posted it ? Thanks im waiting to have this too. |
Thank you so much, I’m wondering if I can use the same approach for emotion |
Since I don't know NextJS, I'm having some trouble figuring out how good this news is for us using React without NextJS. Should we expect things to just work following the React + Styled Components docs now, or do we need to add some special work-arounds or no real options for us yet other than modifying React or Styled Components code? |
is there any plan to support on non framework react SSR apps with streaming? |
@krrssna-g-s The current setup does not require a framework: https://styled-components.com/docs/advanced#streaming-rendering I assume whatever the React 18 solution ends up being wouldn't require one either. |
We spoke with @probablyup a few weeks back and we agreed that it's not worth adding new APIs to aid the current React 18 streaming APIs. I totally forgot to report this back here in the thread. It's very unfortunate - I have some code lying around that implements this outside of Styled-Components (gonna clean it up and share it here if anybody is interested). The problem is that React plans to introduce new APIs to make this way easier for libraries like Styled-Components/Emotion. Of course, the ETA is a little bit unknown - but this is part of the work that is actively worked on in their repository. I'm optimistic that it won't take as long as Suspense/Time-Slicing took 😉 So given this fact... we are put in an uncomfortable position. We could add this new API today, to make it usable today for those who care but we'd have to do a bunch of work to ship something that will soon-ish be deprecated. At the same time, we wouldn't be able to drop it from Styled-Components immediately because the library has to maintain backward compatibility. |
That makes a lot of sense. Thanks for the update! |
@Andarist can you share your solution? Please. Maybe it can help while we wait for the React team to apply the new solutions |
For anyone looking for a hacky way to do this (this doesn't require changes to any libraries but is quite hacky and not tested against every edge case), I threw this together and it seems to work reliably. When react streams content into a page it will always do that by sending a import * as express from "express";
import type { ServerStyleSheet } from "styled-components";
const STYLED_COMPONENTS_VERSION = "5.3.6";
const INITIAL_STYLES_TAG = `<style data-styled="active" data-styled-version="${STYLED_COMPONENTS_VERSION}">`;
const SCRIPT_TAG = "<script>";
export const patchResponse = (resp: express.Response, sheet: ServerStyleSheet) => {
let response = "";
let offset = 0;
let initialStylesInjected = false;
let lastScriptIndex = -1;
let existingStyles = [] as string[];
const decoder = new TextDecoder();
// We patch the "write" method that react uses to output the HTML
const write = resp.write.bind(resp);
resp.write = (chunk: Buffer, ...args: any) => {
const decodedChunk = decoder.decode(chunk);
response += decodedChunk;
const chunkLength = decodedChunk.length;
if (!initialStylesInjected) {
// As soon as we see the first <style> tag, we inject initial styles as
// a <style> tag. For non-streamed rendering this means all styles will
// be included without JS on render.
const index = response.indexOf(INITIAL_STYLES_TAG);
if (index > -1) {
const styles = getNewStyles(sheet, existingStyles);
if (styles.length) {
chunk = inject(chunk, index - offset + INITIAL_STYLES_TAG.length, styles);
}
initialStylesInjected = true;
}
}
// The streamed SSR is updated with script tags that are streamed into the
// page by react. This code finds script tags and injects additional styles
// into them before react hydrates the streamed section. This means styles
// are instantly available.
const scriptIndex = response.indexOf(SCRIPT_TAG, lastScriptIndex + 1);
if (scriptIndex > -1) {
let injectedScript = "";
const styles = getNewStyles(sheet, existingStyles);
if (styles) {
injectedScript += `
document.querySelector("style[data-styled]").innerHTML += ${JSON.stringify(styles)};
`;
}
if (injectedScript) {
chunk = inject(chunk, scriptIndex - offset + SCRIPT_TAG.length, injectedScript);
}
lastScriptIndex = scriptIndex;
}
offset += chunkLength;
return write(chunk, ...args);
};
return resp;
};
const inject = (buffer: Buffer, at: number, str: string) =>
Buffer.concat([buffer.subarray(0, at), Buffer.from(str, "utf-8"), buffer.subarray(at, buffer.length)]);
const SC_SPLIT = "/*!sc*/";
// sheet.getStyleTags() returns ALL style tags every time, so we manually dedupe the styles
// so they're not repeated down the page
// NOTE: data-styled="true" from getStyleTags, but data-styled="active" once it's rendered client side
const getNewStyles = (sheet: ServerStyleSheet, existingStyles: string[]) => {
let styles = sheet
.getStyleTags()
.replace(`<style data-styled="true" data-styled-version="${STYLED_COMPONENTS_VERSION}">`, "")
.replace("</style>", "")
.trim();
for (const style of existingStyles) {
styles = styles.replace(style + SC_SPLIT, "");
}
existingStyles.push(...styles.split(SC_SPLIT));
return styles;
}; |
I had a wrapper class WritableWithStyles extends Writable {
constructor(writable) {
super();
this._writable = writable;
this._buffered = "";
this._pendingFlush = null;
this._inserted = false;
this._freezing = false;
}
_flushBufferSync() {
const flushed = this._buffered;
this._buffered = "";
this._pendingFlush = null;
if (flushed) {
this._insertInto(flushed);
}
}
_flushBuffer() {
if (!this._pendingFlush) {
this._pendingFlush = new Promise((resolve) => {
setTimeout(async () => {
this._flushBufferSync();
resolve();
}, 0);
});
}
}
_insertInto(content) {
// While react is flushing chunks, we don't apply insertions
if (this._freezing) {
this._writable.write(content);
return;
}
const insertion = sheet.getStyleTags();
sheet.instance.clearTag();
if (this._inserted) {
this._writable.write(insertion);
this._writable.write(content);
this._freezing = true;
} else {
const index = content.indexOf("</head>");
if (index !== -1) {
const insertedHeadContent =
content.slice(0, index) + insertion + content.slice(index);
this._writable.write(insertedHeadContent);
this._freezing = true;
this._inserted = true;
}
if (
process.env.NODE_ENV !== "production" &&
insertion &&
!this._inserted
) {
console.error(
`server inserted HTML couldn't be inserted into the stream. You are missing '<head/>' element in your layout - please add it.`
);
}
}
if (!this._inserted) {
this._writable.write(content);
} else {
queueTask(() => {
this._freezing = false;
});
}
}
_write(chunk, encoding, callback) {
const strChunk = chunk.toString();
this._buffered += strChunk;
this._flushBuffer();
callback();
}
flush() {
this._pendingFlush = null;
this._flushBufferSync();
if (typeof this._writable.flush === "function") {
this._writable.flush();
}
}
_final() {
if (this._pendingFlush) {
return this._pendingFlush.then(() => {
this._writable.end();
});
}
this._writable.end();
}
} I can't find the slightly improved version right now but:
|
Hi @Andarist !
thanks! |
This error is thrown only if you |
@Andarist did you ever managed to find your updated version? I wanted to give this a try in a project while we wait for React to release those 'new' APIs to make this all easier |
Is there perhaps a place where one could follow along with the development of the new React APIs? I'd love to get some visibility into what they will look like and when they might be ready. |
Look for anything labeled with Float in the React PRs: https://github.com/facebook/react/pulls?q=is%3Apr+float+ |
I'm quite new to SSR and NodeStreams but using Node docs and styled code i finally got to this:
To get the styles i used a part of styled-components' interleaveWithNodeStream method's code, as the entry param is a
As my dev environment uses UPDATE
The react API method
|
Hi folks,
React 18 will be rolled out in the near future and currently has an RC release that anyone can try out. I thought I would try the new streaming SSR mode with one of the projects I'm working on that uses styled-components and while working on an implementation I realised that styled-components doesn't have a compatible API yet.
Here's a link to the sandbox with the demonstration of the new streaming API by Dan Abramov. React 18 exposes a new function for rendering on the server called
renderToPipeableStream
. As part of it's API it exposes apipe
function that expects a target stream (typically a response stream). The existing styled-components APIinterleaveWithNodeStream
expects a stream exposed by React which makes the whole thing incompatible with each other.I tinkered with the whole thing a bit and the solution seems to be simple - expose a new API that wraps
pipe
function provided by React, returns a new function with the same API that uses the same internalTransformer
stream logic used ininterleaveWithNodeStream
. The version I came up with looks something like this:_getNodeTransformStream
creates the same stream as currently seen ininterleaveWithNodeStream
.I got the whole thing working together and would be glad to contribute this as a PR, however I run into an interesting issue while working on it.
It seems that there's currently a difference in handling SSR on the
main
branch and on thelegacy-v5
branch which I believe is the latest stable release. The difference pretty much comes down to this particular commit. React seems to be emitting rendered HTML in very small chunks, sometimes even without closing the current tag. Here's what I am talking about:Every line in the code sample above is a separate chunk emitted by React. Even attributes on tags are split between multiple chunks. Naturally this does not play well with the current implementation in
main
branch sinceServerStyleSheet
will insert a style tag after every chunk emitted by React which breaks the HTML and leads to garbage data being rendered to user. Interestingly, the implementation inlegacy-v5
works since it does not insert a newstyle
tag if there's no css that needs to be added to stream but this seems like a coincidence rather than something planned.I wonder if it makes sense instead of copying the current logic from
legacy-v5
branch to instead buffer the data emitted by React until it reaches a more reasonable size and then emit it alongside it'sstyle
tag if needed.Would love to discuss it with someone with a deeper understanding of the codebase. I hope I got everything right, I'll happily answer any questions you may have. Any help with this one is much appreciated
The text was updated successfully, but these errors were encountered: