Skip to content

RFC: Streaming SSR and it's impacts #31

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

Open
itsdouges opened this issue Jan 4, 2020 · 18 comments
Open

RFC: Streaming SSR and it's impacts #31

itsdouges opened this issue Jan 4, 2020 · 18 comments
Labels
help wanted 🙋 Extra attention is needed rfc 💬 Request for comments

Comments

@itsdouges
Copy link
Collaborator

itsdouges commented Jan 4, 2020

Edit: Update 6/Oct/2020

Because we've pivoted to atomic CSS things now are made more complicated. We still however need to maintain this story.


A note on design decisions - Right now Compileds primary goal is to introduce constraints that make it impossible to write unperformant CSS in JS.
If we can solve the 90% here that is good enough.

Currently we operate essentially the same as Emotion's default config.

SSR rendered markup:

<style>...</style>
<div class="abc">...</div>

Client markup:

<head>
  <style>...</style>
</head>
..
<div class="abc">...</div>

While this means SSR is 0 config and it just works (with streaming as a bonus) - it makes using any nth selectors annoying/shitty to use.

:first-child
:nth-child
:nth-of-type
:nth-last-of-type
:nth-last-child
:first-of-type
:last-child
* + *

I really want to solve this without re-architecting how things work for the default implementation.
We need to investigate if we can do some CSS magic to create equivalent selectors for the 90% case that:

  1. works for ssr before js has executed (style before the component atm)
  2. works for client after js has executed (style in the head now)

Remember: The style element will move when JS executes. We need to handle both states.

@itsdouges itsdouges added help wanted 🙋 Extra attention is needed new feature 🆕 New feature or request labels Jan 4, 2020
@itsdouges itsdouges changed the title Add work arounds for first child like CSS Investigate work arounds for CSS selectors not working well with inline style element Jan 10, 2020
@itsdouges itsdouges added this to the Phase 2 milestone Jan 10, 2020
@itsdouges itsdouges changed the title Investigate work arounds for CSS selectors not working well with inline style element RFC: First child element workarounds Apr 6, 2020
@itsdouges itsdouges added rfc 💬 Request for comments and removed new feature 🆕 New feature or request labels Apr 6, 2020
@itsdouges itsdouges pinned this issue Apr 10, 2020
@itsdouges
Copy link
Collaborator Author

itsdouges commented Apr 11, 2020

This works for getting around :first-child problems: https://codesandbox.io/s/ssr-first-child-mitigation-7bmsj?file=/src/App.js

+> style:first-child + *,
 > :first-child {
   background-color: blue;
 }

Now it's just a matter of figuring out nth-child - which tbh is infinitely more hard because each child may or may not be a compiled component.

@itsdouges itsdouges changed the title RFC: First child element workarounds RFC: First/nth child workarounds Apr 11, 2020
@itsdouges
Copy link
Collaborator Author

:nth-child workaround could be just a suggestion to code in data attributes.

:nth-child(2n) {
  background-color: gray;
}

<div />
<div /> // highlight
<div />
<div /> // highlight
[data-highlight] {
  background-color: gray;
}

<div />
<div data-highlight /> // highlight
<div />
<div data-highlight /> // highlight

@itsdouges itsdouges changed the title RFC: First/nth child workarounds RFC: First/nth child selector workarounds Apr 11, 2020
@jesstelford
Copy link

It's possible to work around it in emotion too: emotion-js/emotion#1178 (comment)

@itsdouges
Copy link
Collaborator Author

itsdouges commented Apr 12, 2020

hi jess!

yep but you need to opt into the SSR extraction stuff aka "advanced ssr" - instead of that we'll be extracting to CSS files as the "advanced" setup

this is to solve the out of the box SSR problems where it compiles your css into your javascript but results in an inline style element


using the > style:first-child + * selector it can actually solve the :first-child annoyance - it doesn't fix :nth-child though

edit: this doesn't solve if the element in question has multiple style elements associated with it. which it very well could.

@Andarist
Copy link
Contributor

All sorts of patterns are problematic with style tags rendered throughout the tree (consider for example *+* and targeting element deeper in the tree rather than only direct descendants), :not might also come handy in rewriting input selectors.

@itsdouges
Copy link
Collaborator Author

oh that's such a good point!

@itsdouges itsdouges modified the milestones: Table stakes v0.x.x, v1.0 Apr 18, 2020
@itsdouges itsdouges unpinned this issue May 27, 2020
@itsdouges itsdouges removed this from the v1.0 milestone Aug 2, 2020
@TxHawks
Copy link

TxHawks commented Oct 5, 2020

With atomic CSS, this approach will fail in quite a few, much more basic cases than the ones mentioned above, and there has been rather extensive discussion of the issue in other atomic style libraries such as React Native for Web, Styletron and Fela. The Fela Issue is my attempt to try and sum up all possible approaches and pitfalls, so It'd start with that.

In addition to everything mentioned above, @media and @supports might also not work, simply due to unstable and unpredictable variations to the cascade (this is similar to the LVFHA issue discussed here:

<style>
  .a { color: hotpink; }
  @media screen and (min-width: 600px) { .b { color: red; } }
</style>
<div class="a b">Reliably hotpink until 600px and then red</div>
<!-- ... -->

<style>
  .c { color: green; }
</style>
<div class="b c">Always green because @media doesn't add specificity</div>

Worse even, is that this behavior isn't stable. If the second component was to be rendered first, colors would be applied as expected with green turning red at 600px.

One possible solution, initially raised in a comment on the RNW issue is to use inline script tags instead of style tags in SSR, to inject additive styles into the head in the correct order and immediately remove themselves, and thus also avoiding selector issues. Since inline scripts are blocking in the same way style tags are, there should not be any observable difference.

Like anything else, this approach too is a trade-off, since it will not work with JS disabled. The performance implications of this approach should also be considered.

@itsdouges
Copy link
Collaborator Author

love your work @TxHawks, thanks!

like you said everything has its trade-offs, we will have to keep this in mind. another alternative i can think of that wouldn't need a JS solution would be to keep track if there are any nested selectors and always render them, resulting in some duplications

so for example:

<style>
  .a { color: hotpink; }
  @media screen and (min-width: 600px) { .b { color: red; } }
</style>
<div class="a b">Reliably hotpink until 600px and then red</div>
<!-- ... -->

<style>
  .c { color: green; }
  @media screen and (min-width: 600px) { .b { color: red; } }
</style>
<div class="b c">Reliably green undtil 600px and then red</div>

which of course is betting that duplication will be, on average, smaller than the javascript boilerplate

but it's something we need to keep in mind regardless, thanks dude!

@TxHawks
Copy link

TxHawks commented Oct 6, 2020

Thank @Madou

Duplication would indeed work in this naïve example, but in other cases, could also cause unexpected inconsistencies, because it still alters the cascade (definitions bubble up - .b in the second style block also affects the first div. See example here).

This would be fine with the previous example, but would fail in this one, for instance (you'd also have to apply duplication to pseudo selectors, not just nested ones:

<style>
.a { color: red; }
.b:focus { color: blue; }
.c:active { color: purple; }
</style>
<button class="a b c">I'll be unexpectedly blue when active because of the second style block</button>

<!-- ... -->

<style>
.a { color: red; }
.b:focus { color: blue; }
.d:active { color: hotpink; }
</style>
<button class="a b d"> * * * </button>

Progressively rendering style tags into the markup has a ton of edge cases in atomic css and creates a whole lot of extra complexity.

@TxHawks
Copy link

TxHawks commented Oct 6, 2020

Another point is that size-wise, duplication doesn't save the JS boilerplate, it just defers it to a later time. You'd need it anyway to move the content of the different style blocks into the one in head when JS executes.

@Andarist
Copy link
Contributor

Andarist commented Oct 6, 2020

Q: is it any easier to deal with this stuff when creating just a single extracted stylesheet? My guess is not rly - but i havent thought about ot extensively. Definitely it couldnt rely on atomic classes for pseudos and would have to create „per component” ones, right?

@TxHawks
Copy link

TxHawks commented Oct 6, 2020

It's a lot more straightforward with a single stylesheet (extracted or in head). What you'd basically do is figure out the order ruleset types (e.g., lvfha, media queries, etc.) should be in, and have corresponding "buckets" in the stylesheet to which you render rulesets by type (with atomic classes the order inside a bucket is meaningless).

In Fela, we create a style element for each media query, taking advantage of the fact you can specify a media attribute on style elements, and then sort for lvfha inside them.

It's a lot simpler to reason about a single, predictable, source of truth, especially in a language that is order-dependent like CSS.

@itsdouges
Copy link
Collaborator Author

Extracted single atomic sheet is what we are aiming for for the final destination in app, but for the intermediate state ensuring a workable 0 config story is something we must offer.

When we look at trade offs I think there are the must haves and the nice to haves when it comes to streaming support (or in our case, 0 config support)

Must haves would be ensuring that:

  • layout
  • colour

Are stable. And don't change before/after JS executes.

If:

  • pseudo interaction states (hover, focus) not being stable
  • doubling up selectors from duplicate tags

Until JS executes wouldnt be the worst I think. Are there other categories of edge cases you can think of?

@TxHawks
Copy link

TxHawks commented Oct 6, 2020

Feature queries (@supports blocks) also come to mind in the same way as media queries, but I'm sure there are other edge cases I'm just not thinking about right now.

I'm also not at all certain that the JS boilerplate required for the inline script based solution will be larger than duplication, especially in non-trivially sized applications.

As an aside, regardless of how the server-side application of styles will happen, for the client-side style element insertion, I'd recommend Fela's approach to style elements, as it enables a very clear separation of concerns (global, fonts, media query), and by so eases the task of sorting rulesets.

@itsdouges
Copy link
Collaborator Author

itsdouges commented Oct 6, 2020 via email

@itsdouges itsdouges changed the title RFC: First/nth child selector workarounds RFC: Inline style rendering to support zero config Oct 6, 2020
@itsdouges itsdouges changed the title RFC: Inline style rendering to support zero config RFC: Streaming SSR and it's impacts Oct 24, 2020
@mattrq
Copy link

mattrq commented Nov 21, 2024

A solution for the nth selectors

Update the nth selectors so they explicitly exclude or account for Compiled's injected style elements.

From a practical perspective *-of-type can be ignored from the nth selector issue. It's unlikely that non-Compiled style elements will be injected through the document. We allow the *-of-type selectors to help to fix the remaining nth selector issues.

Note: In the examples, style[ data-cmpld] is used as the selector to target Compiled inline styles. A more specific selector could be used to reduce the risk of targeting other inline style elements

Transformation of selectors required

  • :first-child => :not(style[ data-cmpld]):nth-of-type(1)
  • :nth-child(2) => :not(style[ data-cmpld]):nth-of-type(2)
  • :last-child => :not(style[ data-cmpld]):nth-last-of-type(1)
  • :nth-last-child(2) => :not(style[ data-cmpld]):nth-last-of-type(2)
  • * + * => :not(style) + :not(style[ data-cmpld]),:not(style[ data-cmpld]) + style[ data-cmpld] + :not(style[ data-cmpld])
    ^ As Compiled only injects one style element, the selector only needs to support one style element to be added in between the sibling selector
    ^ This same approach can be applied to other sibling selectors

Examples

Screenshot 2024-11-22 at 2 33 08 AM

A working example in code sandbox

Benefits to this solution

  • It works, and reliably
  • No architectural change is needed
  • The selectors used are well-supported by browsers
  • The selectors are not overly complex
  • The selectors are human-readable
  • The selectors work for all cases:
    • SSR's inline style elements in the document body
    • JS client style elements in the document head
    • When CCS is extracted to an external stylesheet
  • The risks are low and more can be done to mitigate them if required

@mattrq
Copy link

mattrq commented Dec 13, 2024

FYI @itsdouges; see above

@itsdouges
Copy link
Collaborator Author

Hey mate. With extraction being the target for production there really isn't a need for inline styles anymore.

Instead we can leverage the style hoisting behavior introduced in React 19 during development and then keep having extraction when building for production.

And then the inline style elements become a thing of the past and we side step this whole issue.

@kylorhall-atlassian if you're interested :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted 🙋 Extra attention is needed rfc 💬 Request for comments
Projects
None yet
Development

No branches or pull requests

5 participants