cssinjs / jss

JSS is an authoring tool for CSS which uses JavaScript as a host language.
https://cssinjs.org
MIT License
7.07k stars 398 forks source link

Full static extraction to CSS file while keeping all dynamic parts intact. #579

Open kof opened 7 years ago

kof commented 7 years ago

The idea is to optimize it in a way that shifts preprocessing runtime overhead to the build stage, while keeping all dynamic parts working. It includes 2 stages: one is babel plugin, another is webpack plugin.

With full extraction,:

With babel plugin only there will be no runtime processing of static styles, only dynamic. Current state is that jss core without plugins with styles object vs preprocessed version of the same object results in 50% performance boost.

Exmple

// source js
const styles = {
  static: {
    color: 'green'
  },
  mixed: {
    color: 'red',
    margin: (props) => props.spacing
  }
}

createStyleSheet(styles).attach()

// generated js
const styles = {
  '@raw': `
    .static-0-0-1 {
      color: green; 
    }
    .mixed-0-0-2 {
      color: red;
    }
  `,
  mixed: {
    margin: (props) => props.spacing
  }
}

const {classes} = createStyleSheet(styles, {
  classes: {
    static: 'static-0-0-1', 
    mixed: 'mixed-0-0-2'
  }
}).attach()

Todo babel plugin

Todo core

Todo webpack plugin

Future enhancements

Some inspiration can be taken from

https://github.com/4Catalyzer/css-literal-loader https://github.com/callstack-io/linaria https://www.npmjs.com/package/extract-jss-webpack-plugin

StephanBijzitter commented 6 years ago

Perhaps inspiration can be gotten from babel-plugin-css-in-js.

The only significant downside with this plugin is that usage with webpack-dev-server (and hot module reloading enabled) does reload the page, as compilation is done in two steps.

  1. user edits styles in a js file
  2. webpack detects changes to .js and hard-reloads page (new .css is now created)
  3. webpack detects changes to .css and hot-reloads page

It's definitely a good improvement, but it can also be made better.

kof commented 6 years ago

Additional thoughts: if we are not allowed to use any dynamic capabilities like function values etc which can access render time variables, why not put js styles in separate files, name them something.styles.js and just write a small script to extract them into css files, it is a few lines of code and not worth creating a package/library for it.

You would need to

  1. require the styles script
  2. use jss.createStyleSheet()
  3. call sheet.toString() => css file

Update:

StephanBijzitter commented 6 years ago

As far as I understand, having the static styles in a different file would essentially be the same as any SCSS/SASS file, except in JavaScript. The main benefit being that you don't need to set up any loaders and linters for those.

It's still a very nice improvement, but for me the first reason to use JSS is to eliminate unused styles from being present in whatever is served to the user. Dead code elimination simply doesn't work with CSS (unless I missed something) and with JavaScript it's already present (provided you don't mess it up yourself). Do you think that would still work with separate JSS style files?

kof commented 6 years ago

There are 2 types of unused styles:

  1. Styles you didn't use yet, for e.g. during ssr single pass.
  2. Styles you never use

Currently we don't do any of these. Uglify will not drop any styles property because it doesn't know if a corresponding classes.rule is used or not.

For SSR we could add something to JSS, evtl. as a plugin for e.g. using getters or proxy that can mark rules as used. In that case separate file or the same doesn't matter.

In case of dead code, there are 2 possibilities:

  1. Define each rule as a separate variable: var btnCls = css(...) so that uglify can see an unused reference. Also eslint has a rule for that. This requires a different API.

  2. Write an eslint rule which is smarter and can check for usage in classes map. It will have to know jss, react-jss, styled-jss interfaces. It can as well work with external files.

kof commented 6 years ago

Update the original description, since I see that we can actually keep both static extraction and dynamic styles accessing the runtime.

satya164 commented 6 years ago

Think of potential solution to the problem: {padding: (props) => props.spacing, paddingLeft: 10} after compilation paddingLeft will be overwritten by padding since it will have higher source order specificity

I think there are 2 ways to handle this:

  1. The plugin can completely ignore rule-set which cannot be statically evaluated
  2. The plugin can keep a list of what's safe to re-order and what is not, and then extract the subset of the rules statically which is safe to re-order. I think CSS minifiers already do this.

I think the first solution is the most logical solution for initial implementation and eventually, the plugin can get smarter about it.

kof commented 6 years ago

@satya164 I was thinking to not evaluate function values at all and always keep them dynamic and warn the user if his ordering is going to be broken, based on prop names and dynamic values.

satya164 commented 6 years ago

Yeah, that's mostly what I meant by the first one :) But no reason to get smarter in future and handle it as well :)

kof commented 6 years ago

Updated again, I figured out with help from @giuseppeg that

  1. babel plugins should not emit css files, its an ugly side effect
  2. we can make babel plugin very useful by its own by preprocessing static styles and keeping them in js in the first step, even without static extraction, rendering will be much faster
  3. @raw rule will be handled by a new plugin, which will basically just take the string as it is and render it, this should be 0 processing overhead
marmikdesai commented 5 years ago

Hi, Is there any timeline so we can use this in production?

Mati365 commented 5 years ago

@kof Isn't extracting styles during webpack compilation too overcomplicated? Maybe extracting static styles during server startup to one big sheet and adding information about 'critical' classnames to components uuids will be better? I think that creating hash with components class names improve hydration performance - client won't have to manually remove SSR stylesheet after hydration

Consider this:

const {css} = createSheet();
const {classes} = css(
  {
    base: {
      display: 'initial',
      background: 'red',
    },

    green: {
      background: 'green',
    },
  },
);

const Component = () => <div className={classes.base} />;

const CSS = await sheet.compileCSS();
// which compiles to something like it:
{
  __styles: `
    .c0 {
      display: initial;
      background: red;
    }
    .c1 {
      background: green;
    }
  `,

  ...
  [componentUUID]: {
    base: 'c0',
    green: 'c1',
  } 
  ...
}

runServer(CSS); 
Jony-Y commented 2 years ago

Any update on this?