Skip to content
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

Streaming rendering support #334

Open
rtsao opened this issue Aug 23, 2019 · 5 comments · May be fixed by #394
Open

Streaming rendering support #334

rtsao opened this issue Aug 23, 2019 · 5 comments · May be fixed by #394

Comments

@rtsao
Copy link
Member

rtsao commented Aug 23, 2019

This idea was initially proposed in #137 but I think there's a fairly good way of doing this.

On the server:

  • Produce a stream of <style> tags from atomic engine
  • Merge the <style> tag stream with the stream returned from ReactDOM.renderToNodeStream

On the client:

  • Prior to ReactDOM.hydrate, hoist the contents of all the inline <style> elements into <head> and remove the inline <style> elements (while hydrating the cache as normal)

I think this would work flawlessly aside from the following (likely exceedingly rare) corner cases where interleaving of streams results in a <style> element being inserted:

A) as the first child in an element containing children styled with :first-child
B) as the last child in an element containing children styled with :last-child
C) into an element containing children styled with :nth-child
D) between two elements styled with ~ selectors

If this were to happen, then streaming rendering would cause incorrect styling (at least until the offending inline <style> is removed during hydration).

@TxHawks
Copy link

TxHawks commented Dec 2, 2019

I believe there are other cases where this could fail. Reliance on the cascade is indeed significantly decreased with atomic css, but it is not completely gone.

For instance, the following could break until rehydration:

// ComponentA.js
import { useStyletron } from "styletron-react";

export default () => {
  const [css] = useStyletron();
  return (
    <div
      className={css({ 
        backgroundColor: "#00f",
        "@media (min-width: 600px)": {
          backgroundColor: "#f00",
        },
      })}
    >
      I'm blue and turn red in screens wider than 600px
    </div>
  );
};
// ComponentB.js
import { useStyletron } from "styletron-react";

export default () => {
  const [css] = useStyletron();
  return (
    <div
      className={css({ 
        backgroundColor: "#0f0",
        "@media (min-width: 600px)": {
          backgroundColor: "#f00",
        },
      })}
    >
      I'm green and turn red in screens wider than 600px
    </div>
  );
};

If because of streaming, server rendered html turns out to be:

<style>.a{background-color: "#0f0"}</style>
<style media="(min-width: 600px)">.b{background-color: "#f00"}</style>
<div class="a b">I'm blue and turn red in screens wider than 600px</div>
<!-- ... -->
<style>.c{background-color: "#00f"}</style>
<div class="b c">I'm green and turn red in screens wider than 600px</div>

The second div will always remain green, because .c comes after .b in the cascade, at least until rehydration, when media query ordering takes place.

I'm aware that this can be avoided by using a max-width media query around .c, but this should in the very least be considered a tradeoff and made very clear in the docs. It would basically mean that media query ordering is no longer a thing

@rtsao
Copy link
Member Author

rtsao commented Dec 6, 2019

That's a good point! One possible ordering mechanism which I've contemplated in the past is using repeated selectors to enforce precedence regardless of order.

For this particular case, all media query styles could simply have a repeated class selector:

<style>.a{background-color: #0f0}</style>
<style media="(min-width: 600px)">.b.b{background-color: #f00}</style>
<div class="a b">I'm blue and turn red in screens wider than 600px</div>
<style>.c{background-color: #00f}</style>
<div class="b c">I'm green and turn red in screens wider than 600px</div>

This would maintain proper precedence of media query styles and regular styles.

However, I think the precedence of media query styles amongst themselves would still be a problem.

Prior versions of Styletron didn't make any guarantees about overlapping media queries on the same element (the onus on users to avoid overlapping queries), but it does now.

I think ordering the media queries would be difficult using this technique, as you'd need to be able to arbitrarily render styles at specificities between already-rendered styles. Even if you tried to do this, in the worst case, this might be impossible because two selectors may already have adjacent specificities.

So I think for progressive rendering, it may be required to have a "strict mode", where warnings are issued for usage of overlapping media queries on the same element and any usage of :first-child, :last-child, :nth-child, or ~.

Then, if there are no warnings, progressive rendering could be safely used without caveats.

@airhorns
Copy link

Has anyone made any progress on this sucker? It'd be really awesome to get in, especially as renderToNodeStream starts to take a bigger seat with Suspense and Concurrent mode in React

Also, one idea I came across was using inline <script/> tags instead of <style/>s here atlassian-labs/compiled#31 (comment). The script tags would run synchronously, and could write styles to the <head/> and then remove themselves, leaving no element in the DOM to screw up the sibling selectors. Seems like a teensy bit of boot time overhead but fixes all the correctness issues, and has the added bonus of not needing more DOM preprocessing before hydrating with React to avoid server/client mismatches.

@frenic
Copy link
Collaborator

frenic commented Feb 18, 2021

There's similar solution in Glitz. But it renders inline <style> tags which hydrates this into <head> and gets removed later on. It works quite well too.

@AyoubElk AyoubElk linked a pull request Mar 16, 2021 that will close this issue
3 tasks
@SukkaW
Copy link

SukkaW commented Feb 24, 2022

That's a good point! One possible ordering mechanism which I've contemplated in the past is using repeated selectors to enforce precedence regardless of order.

For this particular case, all media query styles could simply have a repeated class selector:

<style>.a{background-color: #0f0}</style>
<style media="(min-width: 600px)">.b.b{background-color: #f00}</style>
<div class="a b">I'm blue and turn red in screens wider than 600px</div>
<style>.c{background-color: #00f}</style>
<div class="b c">I'm green and turn red in screens wider than 600px</div>

This would maintain proper precedence of media query styles and regular styles.

However, I think the precedence of media query styles amongst themselves would still be a problem.

@rtsao @TxHawks FYI, this is exactly how Facebook handle their atomic CSS at facebook.com:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants