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

"Extract CSS File" seems misleading #86

Open
RussianCow opened this issue Mar 18, 2018 · 23 comments
Open

"Extract CSS File" seems misleading #86

RussianCow opened this issue Mar 18, 2018 · 23 comments

Comments

@RussianCow
Copy link

First of all, thank you so much for your work in compiling this table! It's an awesome reference.

Now, my question: How do you determine whether a library supports CSS file extraction (under the "Extract CSS File" column)? As an example, aphrodite has a checkmark under that column, but this issue suggests that this is explicitly not a feature of the library. Other libraries that have that checkmark in the table, such as glamorous, don't suggest any way of accomplishing this.

Either I'm misunderstanding what that column means, in which case it might be a good idea to describe it in the readme, or some of these values are incorrect. If it's the latter, I would be more than happy to go through them and compile a list of which of these libraries actually advertise this as a feature.

Thanks!

@RussianCow
Copy link
Author

A few minutes after writing this, I discovered that you have an example of CSS file generation in each of the example directories. My bad for not seeing that sooner. 😁 However, I think using the word "extract" here is misleading, because (at least to me) it implies something that statically extracts all the generated CSS into a file at build time (a la extract-text-webpack-plugin), whereas your examples generate a CSS file dynamically based on what was rendered by React. Maybe this is just my own personal bias showing (I've always used extract-text-webpack-plugin with LESS/CSS modules to generate static CSS files at build time), but the wording in the table seems misleading, because this method doesn't actually allow you to extract a full, reusable CSS file that you could use for the whole app, since it depends on what was rendered. An example of a CSS-in-JS library that does support the kind of file extraction I'm talking about is emotion (though admittedly, it's very limited in how you are allowed to use it).

Do you have any thoughts on how to make this more explicit?

@streamich
Copy link
Contributor

Out of interest, why do you want to extract CSS into an external .css file?

@RussianCow
Copy link
Author

Mostly performance reasons for server-side rendering. I'm working on a small project that has to support clients with JS disabled, so the app is "universal" (or isomorphic or whatever term you prefer 😉). Generating a static CSS file is a requirement because then it gets cached in the browser forever, rather than having to serve different CSS with every page load (which is what I would have to resort to with most CSS-in-JS libraries). I otherwise prefer the CSS-in-JS approach from a developer experience standpoint, so I was curious as to whether any libraries gave you the best of both worlds, and stumbled upon this repo in my search.

I understand my use case is probably not very common, so feel free to ignore me. 😁 I have just always seen the word "extract" used in the context of things like extract-text-webpack-plugin that extract the CSS statically at build time, so this threw me off a little bit.

@stereobooster
Copy link
Collaborator

stereobooster commented Mar 18, 2018

so the app is "universal" (or isomorphic or whatever term you prefer 😉)

the word you are looking for is graceful degradation e.g. with SSR and extracted CSS browser will be able to render page even if JS is disabled

Extracting CSS and prerendering HTML (with SSR) improves First Paint significantly.

understand my use case is probably not very common

It is pretty common - I mean requirement to generate CSS as text upfront would it be separate file or inlined in HTML in header.

@streamich
Copy link
Contributor

I was asking because I am currently working on nano-css and there all raw CSS is available as a string in nano.raw property. I think the general approach (especially for single page apps) is to inline that string into server rendered HTML, which should be faster than doing one extra request for .css, for example, Google AMP requires you to inline CSS.

You could actually serve that string on a server as a separate file, too.

A bit more problematic would be to extract all styles at build time. I am thinking, one could simply at build time require all .js component files, that would build static CSS in nano.raw and that can be saved to disk. Do you see any problems with that globbing for all files in /components folder and simply requiring them?

@RussianCow
Copy link
Author

RussianCow commented Mar 18, 2018

@stereobooster To clarify, when I said it's not common, I meant it's likely not common for people using CSS-in-JS solutions (at least, that seems to be my experience). This works just fine if you use plain CSS or a preprocessor like LESS or Sass, but I think most people who go the CSS-in-JS route are aware of the drawbacks and don't care about having all the CSS up front. In fact, you could argue that is one of the advantages of doing CSS-in-JS: it only renders the minimal CSS necessary for any given render in most cases.

@streamich I think simply requiring the components and pulling out nano.raw in a script would be pretty finicky, because if your JS relies on any preprocessing steps like babel, then that script would have to go through your build process before it gets run, which is one of those awkward meta things that's hard to deal with. A more general approach would be to write a babel plugin (or similar), as then the build system (e.g. webpack) can then extract that data as the files get parsed/compiled, instead of in a separate step. (This is how emotion does it.) However, this means all your style generation code has to be parseable statically, at compile-time, which limits what you can do with it (i.e. you can't have dynamically-generated styles that depend on runtime values). From a brief look at your library, I don't think this would be possible, at least not generally.

@stereobooster
Copy link
Collaborator

There is linaria - CSS is extracted at build time, no runtime is included

@RussianCow
Copy link
Author

@stereobooster Awesome, that's exactly what I was looking for! Eventually I would have gone through the whole list, but that saves me a lot of time. :) Thank you!

@RussianCow
Copy link
Author

To go back to the topic of this issue for a second, I just checked out your feature definitions here, and "zero runtime dependency" fits what I was looking for, I just hadn't thought of it that way. So if that page is representative of how the features of these libraries will ultimately be described, I think that solves this issue.

@stereobooster
Copy link
Collaborator

So if that page is representative of how the features of these libraries will ultimately be described, I think that solves this issue.

that was the idea, but we are not quite there yet

@streamich
Copy link
Contributor

Just wanted to add here, that one of the main reasons to use CSS-in-JS is to be able to use component or render method variables in your CSS, like props. In that case I don't see how CSS can be extracted, or are there libraries that do that?

@RussianCow
Copy link
Author

RussianCow commented Mar 19, 2018

@streamich Agreed, I think that's the main reason people use these solutions, so it makes sense that static file extraction isn't a top priority for (or even supported by) a majority of CSS-in-JS libraries. Having said that, stereobooster pointed out that linaria is one library that does everything at build time, so there is at least one example.

Edit: To clarify, you can't have both simultaneously—either you have dynamic styles that are based on props and other runtime values, or you have static styles that can be extracted ahead of time. Although, a solution that let you mix and match the two approaches would be interesting. :)

@stereobooster
Copy link
Collaborator

is to be able to use component or render method variables in your CSS, like props

Yes, but most of the time this is not random value, but rather limited set of values which represent all possible states of application, like in Redux. So you can compile CSS-in-JS down to static CSS with limited number of classes which corresponds to all states

@streamich
Copy link
Contributor

streamich commented Mar 19, 2018

@stereobooster

So you can compile CSS-in-JS down to static CSS with limited number of classes which corresponds to all states

Has anyone done that? Or, at least, maybe just the default value could be extracted as external CSS.

@RussianCow
Copy link
Author

@stereobooster But to do that you would have to somehow know every possible state of your application, which isn't possible (at least not generally—you could just render the app and grab the generated CSS, but that would only work assuming none of the variables ever change). Even Redux doesn't magically know all the possible states of the store, it just helps you be more explicit about state changes. That's why you're limited in what you can do if you want support for static CSS extraction.

@streamich I would be interested in a solution that combines the static and dynamic approaches, where it would statically compile as much CSS as it can, and dynamically load the rest. But I'm not aware of a library that does this, and it seems like that would take a lot of work to implement.

@streamich
Copy link
Contributor

streamich commented Mar 20, 2018

@RussianCow I thought about it a bit and I think it is kinda doable.

rule() and sheet() interfaces — rule() is basically the one Linaria provides — should be straight-forward: there would be a script maybe as part of Babel or Webpack that at build time either detects component modules or uses a glob pattern to find them. Then it simply evaluates those modules, I believe this is what Linaria does (for example, because it requires no side-effects). Once executed all CSS would be simply available in nano.raw as a string.

For jsx() interface it would extract the static CSS automatically, as it internally uses rule(). However, we would not be able to extract the dynamic styles of the jsx() interface, they would need to be accumulated as a string and injected into HTML on an actual page request.

Then style() interface actually uses jsx() internally, but — because its dynamic CSS is a function of props — one could for example run that function with no props or with default props, that way extract the "default" dynamic CSS.

And here is little summary grouping interfaces by generation:

  • 3rd generation: can extract into external style sheet at build time
  • 4th generation: can extract static styles and dynamic styles, but for dynamic styles would need to run the template with no props or default props
  • 5th generation: can extract static styles, but not dynamic ones

Tell me if I'm missing something.

@RussianCow
Copy link
Author

@streamich This is what Linaria does (the 3rd generation part at least), so I think you're right, that will work. However, I personally don't think evaluating modules to get their style info is a good strategy because it means your code cannot depend on any preprocessing other than babel (such as webpack loaders, as noted in the issue here). Myself, I make heavy use of file-loader, so this is a deal-breaker for me, but I can see this issue arising with other build setups as well.

Is there no reason you couldn't just extract all of that info with a babel plugin, without needing to evaluate the file? What do you gain by doing that?

PS: I like that separation of libraries by generation; it's very helpful.

@RussianCow
Copy link
Author

Actually, I just realized why Linaria does it that way. The key is in this comment and the one after it:

Correct me if I’m wrong, but Linaria needs to import the dependencies of the current file in order to run the code that's interpolated in the CSS, right? At that point, the current file’s dependencies have not yet been processed by Webpack, as the references have just been encountered. If my assumptions are correct, it seems pretty clear why it doesn’t work. In fact, I don’t see how it can work. Short of rewriting it as a Webpack plugin.

So evaluating the modules is necessary in order to properly parse styles that use references from other files, like this:

import colors from './colors'
const style = css`
  color: ${colors.blue};
`

Then the solution I want wouldn't be possible with babel alone; you'd need a plugin for the bundler (e.g. webpack) to then follow and process all of the references after the bundle has been built. So, it's possible, but definitely not easy, and the solution would be tied to webpack or whatever bundler(s) you choose to write a plugin for.

@streamich
Copy link
Contributor

streamich commented Mar 20, 2018

@RussianCow

Is there no reason you couldn't just extract all of that info with a babel plugin, without needing to evaluate the file?

One big reason for using CSS-in-JS is the power JavaScript gives you. Like variables

const className = rule({
  color: require('../my-theme').mainColor
});

Mixins:

const baseButtonStyles = {
  border: 0,
  background: 'grey',
};

const styles = sheet({
  button: baseButtonStyles,
  primary: {
    ...baseButtonStyles,
    background: 'blue'
  }
});

Environment vars:

const className = rule({
  outline: process.env.DEBUG ? '1px solid red' : '0'
});

And lots of other things can imaging...

...and the solution would be tied to webpack or whatever bundler(s) you choose to write a plugin for.

Yes, but everyone uses Webpack :) Also, this problem arises only if you use Webpack loaders (but then again, everyone uses those :( ) Yeah, I guess it would have to be Webpack plugin.

@RussianCow
Copy link
Author

@streamich Right, I was asking about whether you had to actually evaluate the file in order to compile that info. Looks like you do unless you write a webpack plugin that follows all the references after the fact. So yeah, it would have to be either a babel plugin, and just not support loaders or any other kind of preprocessors, or a webpack plugin, which sounds harder to implement (I don't know) and would only work with webpack, but would work with no limitations on your code or build process.

I guess you could write the plugin such that it would try to compile whatever styling it can statically, and if there is anything dynamic that it depends on (for instance the return value of a function call), it would leave that to be "compiled" at runtime.

@streamich
Copy link
Contributor

streamich commented Mar 22, 2018

@RussianCow

I've create an extract addon, that does something like discussed above. It is not a complete solution, but just the lowest level, on top of which Webpack plugin or Babel one can be built.

Here I created a demo:

@stereobooster
Copy link
Collaborator

stereobooster commented Mar 23, 2018

@stereobooster
Copy link
Collaborator

One more example of "extraction" - http://css-blocks.com/

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

No branches or pull requests

3 participants