Emotion: feature: static sheet cacheable by browsers (and with IE11 support)

Created on 12 Jul 2017  路  5Comments  路  Source: emotion-js/emotion

Hey @tkh44 we talked on Twitter, I figured I formalize what I was talking about somewhere.

Basically, I have an opinionated vision for CSS, and I had planned to do it myself, but if it belongs in a package that already is fighting the "extract css" fight, then that may make more sense. So here are the opinions:

  • forgoing statically-generated stylesheets cacheable by browsers is a mistake
  • any render time "work" for styles that are otherwise static is a mistake

Browsers and CDNs cache stylesheets for a reason. Performance. Forgoing cacheable stylesheets means larger javascript bundles, more work on the server, and more work in each update on the client.

The last point is especially important when it comes to a style library since it's closely related to animations, and anyone who's made thoroughly animated apps with React knows dropping frames in conjunction with component instance updates is a constant battle. So for a style library to be adding extra render time cycles is a mistake.

I love the APIs of Styled Components, Glamorous, and now Emotion. It makes a lot of things stupid simple. But then you're missing off on these traditional perf wins. So what's the in-between solution?

  • compile static styles into stylesheets at build time
  • focus on writing your styles in javascript (i personally think the future is just javascript). Evaluate that javascript at build time--i.e. giving you the full power of javascript--and generate sheet(s).
  • since your styles no longer have access to runtime/render time variables, you're left with environment variables (such as whether your platform is React Native or web, but not things like window size--use percentanges, calc, vh and vw instead). This is fine, because:
  • for dynamic styles proceed as normal assigning them to style within your components
  • use something like extract-css-chunks-webpack-plugin to insure you're getting a stylesheet per javascript chunk (this handles the 80% case for not sending useless styles, and also results in less bytes sent than render-path solutions since you have to send the definition of your CSS as javascript anyway ).

Not sure if you read my Webpack "dual-import" article:
https://medium.com/@faceyspacey/webpacks-import-will-soon-fetch-js-css-here-s-how-you-do-it-today-4eb5b4929852

But basically the extract-css-chunks-webpack-plugin and babel-plugin-dual-import allow you to do what Webpack will be doing soon, which is: fetching both a CSS + JS chunk per call to import(). My opinion is obviously that's the way it's supposed to be. That means the past, present, and future is bright for cacheable stylesheets.

V2:

  • those dynamic styles could in fact be accounted for (and should). Basically, if you have a function that takes props and returns a style, the babel-plugin should keep that in JS, and combine the class of the static style with its result in the created <StyleComponent />.

So from the user's perspective, the API would be the same to what you have now. They just also have to do some work to insure they serve those stylesheets to the browser (both on initial load in in calls to import(), which my tools make super easy).

As for no IE11 support within the current incarnation of "extract mode," extract mode is amazing and why I'm reaching out to you, but I mean who can use that besides people building tools for developers, like CodeSandbox? It basically can only be used for developer-only products. You can't make a serious product and forgo modern IE. The above static solution (which should no longer generate CSS variables will handle that).

V3:

  • React Native support always has to be in the back of our heads. In RN, StyleSheet.create is called with the styles instead of generating anything at build time.
  • various transformations back and forth between RN style definitions and traditional css definitions. ideally vice versa as much as possible
  • things like calc(100vh - 200px) need to be transformed to Dimensions.get('window').height - 200px
  • for this solution to wear the "crown" and finally put to rest the CSS problem, your styles have to be used as interchangeably as absolutely possible between web and RN.

So that's my take. Obviously a lot of what's gone on in the React/CSS world has to do with server-rendering, minimizing CSS bytes size, and making it extremely easy to serve CSS while simultaneously code-splitting. But it's been done wrong in my opinion. Browser-cacheable stylesheets cannot be left out. Sokra's plan as described in the dual-import article is the correct one. What this does is also solve the "bytes size" problem that render-path solutions try to solve--i.e. where they send the most minimal amount of CSS as consumed during the render path. In short render path solutions aren't the only way to solve that. In fact, it's worse--you're sending the style definitions in your JS anyway. So the total bytes sent is more than dynamic split css chunks. And you're also losing cacheability in other more advanced ways too: with render-path solutions, if either ur js or css changes, the cache is busted for both. Whereas if they are independent (and say you don't touch your styles as much), changes to js won't cache bust the css.

Final benefit:
If you've read Dan Abramov's take on CSS modules in CRA (which are not supported), you will see he's against incorporating it (for several smart reasons, given their aims of targeting non-power-users in a non-webpack-specific way that will future-proof them if they ever leave webpack). I haven't tried, but I assume Glamorous and this work nicely with CRA. Well, the above technique will still work in CRA, and then when you eject and want to go "pro" you can add a babel-plugin (loader, etc) that creates the stylesheets for you. So your same code from CRA still can get professional cacheability and all the benefits of this solution. More importantly it still achieves what CSS Modules do (well, after you eject) in a way CRA can support. I think the CRA team would be very excited about that. That way you don't have to use CRA and feel like you're doing it the baby way. You have a future-proof way to do your CSS (and a more professional way compared to having to use global styles).

So that's my take. I love the interface, but I don't like the performance costs. I'm also earnestly unclear why the performance benefits are something everyone was so keen to forgo. Why has this happened?

Most helpful comment

Thanks for taking the time to write this up @faceyspacey. I might want to point you to https://github.com/tkh44/emotion/issues/62 where I laid out some of our plans. I believe they align with yours except we use css vars with a CSSOM insertRule fallback. We are already getting really close to vanilla react with no styles applied.

We already do some optimization tricks that end users can't see. If a css block is static there is no function call. It's just a string.

const className = css`color:blue;`
<div className={className} />

and

const className = 'blue-text-class'
<div className={className} />

are the exact same except for the class name after emotion is run

The first example is compiled to:

const className = 'css-className-1234a'
<div className={className} />

I also have a branch already using the React inline styles for dynamic values
https://twitter.com/tkh44/status/875187237280940033

All this is to say we have very similar goals in mind.

All 5 comments

Anyway, the "crown" could be yours my friend lol. It's yours for the taking. You're clearly the closest anyone has gotten to attaining this "best of all worlds" path.

ps. I planned to call it "Crown CSS" when I got around to making it. That's the joke--but I think it's only in my head :)

To be perfectly honest, I didn't read the whole thing (yet!) but I will say that I am in your camp. I've been completely against the whole CSS-in-JS boat since its inception for those 2 reasons alone.

However.

I'm paying attention to // looking at emotion because it's the only one that actually allows you to _extract_ static styles out and into a typical/old-school stylesheet. It only keeps the "moving pieces" in its runtime... moving the rest to an external, cacheable file.

Essentially the reason I've reached out to Emotion is because they do exactly that (extract into external stylesheets). And that's a first. So what I wrote is just extending/altering that path/vision. Hopefully since @tkh44 has already started down this route, it's a lot shorter path to take.

If the css vars thing from "extract mode" worked in IE11, this would be it, and the story over. I didn't even think of that. But since that's not the case, a slightly different route has to be taken.

If I understand the conversation here correctly I think I have been knocking on the same doors as you both have about extracting css. Ideally all static css should be extracted no matter the mode (imo, maybe a separate mode to do this to make it opt-in). Some discussion on this can be found here: https://github.com/tkh44/emotion/issues/62 and this really early WIP PR here: https://github.com/tkh44/emotion/pull/121

Thanks for taking the time to write this up @faceyspacey. I might want to point you to https://github.com/tkh44/emotion/issues/62 where I laid out some of our plans. I believe they align with yours except we use css vars with a CSSOM insertRule fallback. We are already getting really close to vanilla react with no styles applied.

We already do some optimization tricks that end users can't see. If a css block is static there is no function call. It's just a string.

const className = css`color:blue;`
<div className={className} />

and

const className = 'blue-text-class'
<div className={className} />

are the exact same except for the class name after emotion is run

The first example is compiled to:

const className = 'css-className-1234a'
<div className={className} />

I also have a branch already using the React inline styles for dynamic values
https://twitter.com/tkh44/status/875187237280940033

All this is to say we have very similar goals in mind.

Was this page helpful?
0 / 5 - 0 ratings