jss icon indicating copy to clipboard operation
jss copied to clipboard

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

Open kof opened this issue 7 years ago • 12 comments

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,:

  • There will be no static styles at runtime that need to be parsed and injected (only dynamic)
  • No double loading of static styles in JS build + CSS build.

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

  • [x] Identify injectSheet(styles, options), createStyleSheet(styles, options) calls, make it customizable for different function names.
  • [x] allow to pass jss config to the babel plugin
  • [x] extract static styles object literal
  • [x] extract static styles from the reference
  • [x] extract any value from reference
  • [x] extract sheet options if provided
  • [x] extract sheet options from reference
  • [x] extract sheet options nested properties from reference
  • [x] create jss with plugins => createStyleSheet(styles, options).toString()
  • [x] insert static css as a @raw rule into the styles declaration
  • [x] remove original static styles
  • [x] pass classes map from static sheet as options
  • [x] math expressions
  • [x] function call results
  • [ ] css preprocessing pipeline (with postcss)
  • [ ] autoprefixer
  • [ ] babel like theming configuration over file system in any directory????

Todo core

  • [x] implement option classes to createStyleSheet
  • [x] implement@raw plugin, add it to default preset
  • [x] make a bench comparing @raw with equivalent style objects
  • [ ] docs
  • [ ] blogpost?

Todo webpack plugin

  • [ ] identify @raw rule
  • [ ] extract css
  • [ ] remove the rule
  • [ ] provide the css to webpack so that other loaders can use it (tbd how)

Future enhancements

  • 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
  • If static CSS is used without critical CSS over SSR, dynamic styles are not part of the static bundle and styling is incomplete. Think of a strategy to warn/require default values for dynamic styles.
  • a demo app with webpack/postcss/css-modules/autoprefixer
  • When there are no dynamic styles, remove all jss runtime code from the module
  • When function values/rules are not using props and can be statically extracted (see linaria)
  • Separate entry point jss/static for a reduced version of jss which does not include any plugins etc logic, since it is all preprocessed (unless we can treeshake it???)
  • Remove unused styles or warn when any detected, see this and the article for e.g.

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

kof avatar Sep 07 '17 20:09 kof

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.

StephanBijzitter avatar Sep 22 '17 11:09 StephanBijzitter

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:

  • This would make for bad DX, since user would have to manually separate static and dynamic styles
  • You would be double loading static styles: once with css bundle, once with js bundle

kof avatar Apr 02 '18 09:04 kof

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?

StephanBijzitter avatar Apr 02 '18 10:04 StephanBijzitter

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 avatar Apr 02 '18 16:04 kof

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

kof avatar Jul 01 '18 13:07 kof

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.

satya164 avatar Jul 01 '18 19:07 satya164

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

kof avatar Jul 01 '18 19:07 kof

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

satya164 avatar Jul 01 '18 21:07 satya164

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

kof avatar Jul 12 '18 17:07 kof

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

marmikdesai avatar Apr 04 '19 05:04 marmikdesai

@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); 

Mati365 avatar Aug 21 '19 13:08 Mati365

Any update on this?

Jony-Y avatar Jun 21 '22 12:06 Jony-Y