atlassian-labs / compiled

A familiar and performant compile time CSS-in-JS library for React.
https://compiledcssinjs.com
Apache License 2.0
1.99k stars 67 forks source link

RFC: Streaming SSR and it's impacts #31

Open itsdouges opened 4 years ago

itsdouges commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

: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
jesstelford commented 4 years ago

It's possible to work around it in emotion too: https://github.com/emotion-js/emotion/issues/1178#issuecomment-469098353

itsdouges commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

oh that's such a good point!

TxHawks commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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:

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

If:

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

TxHawks commented 4 years ago

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 commented 4 years ago

Cheers will do 🙂

cc @pgmanutd had some good discussion today ^

On Tue, 6 Oct 2020, 6:28 pm Jonathan Pollak, notifications@github.com wrote:

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 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.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/atlassian-labs/compiled/issues/31#issuecomment-704086232, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABT4PHNB5ORJWFLXC6ZH7TTSJLBKLANCNFSM4KCVWUBQ .