atlassian-labs / compiled

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

RFC: Compress atomic class names during CSS extraction #335

Open itsdouges opened 3 years ago

itsdouges commented 3 years ago

Once we have implemented the first pass of CSS extraction opportunities to optimize the output will present themselves. One such optimization will be compressing groups and values.

One example is:

<div class="_1wyb1fwx" />

Which is generated from:


styled.div`
  font-size: 12px;
`;

We do this to reduce the potential size of the class name and ensure it can be deterministically used to both enable composition (style overrides) and ensure only one declaration exists.

The deterministic part is important here - because you can import compiled components without needing to configure your bundler - like the current age of CSS in JS libraries the "0 config story" still works.

But this does come at a cost - bundle/HTML size. When running CSS extraction we get another opportunity to perform optimizations - and because it's only to be used in "applications" we know that at the time of build we have knowledge of every compiled component used in the app.

What is the smallest possible representation of a classname? Keeping in mind we want the following to still function deterministically:

The obvious pragmatic answer is - do both. For components where they can't receive a className AND they are applying to a DOM element - we can safely assume they don't need overrides. We can compress their slectors.

  1. Keep original atomic rules
  2. For any safe components with rules, compress them down to a small representation _syaz18rw to a

If it's safe, it would transform down to:

<span className="a b c" />

If it's not safe, it would stay as:

<span className={ax(['_syazxdf1', '_1p1dangw', '_k48p1fw0', props.className])}>

And the CSS rules end up having one new selector:

.a, ._syazxdf1 {}

.b, ._1p1dangw {}

.c, ._k48p1fw0 {}

And so on.

itsdouges commented 3 years ago

Tangent thought: Explicit disabling of composition/conditional CSS. It means when baked we can simplify it down to something tiny.

albertogasparin commented 1 year ago

I'll add an idea for a step in between: having better mapped shorthands for known properties. For instance, instead of width: 10px becoming _1p1dangw, why cannot we make it just _w_10? CSS has a limited dictionary and we could provide compact shorthands for the most used properties. For values, we could expose a babel config to declare the most used unit (px/em/rem/...) and compact those while preserving hashes for all others. It might not be a massive saving but given how often things like position: relative, padding-left: Xunit, ... are used, might have sizeable impact on DOM (even if gzip compression mitigates a lot)

liamqma commented 1 year ago

Hey @albertogasparin ,

For values, we could expose a babel config to declare the most used unit (px/em/rem/...) and compact those while preserving hashes for all others.

Can you give an example please?

albertogasparin commented 1 year ago
[
  "@compiled/babel-plugin",
  {
    "parserBabelPlugins": ["typescript", "jsx"],
    "mostCommonUnit": "px"
  }
]

This is leveraging the fact that projects generally have a recommended unit of measure that most values adopt (usually px or rem). But thinking more about it, in our case with the push for css vars for spacing, we might find very little benefit as most might end up being padding: var(--spacing-xs) 🙈

sachin-hg commented 1 year ago

We really liked the idea of this RFC and implemented something which works.

Our implementation focuses on @linaria/atomic. But i guess a similar implementation should be possible with in compiled as well.

Because both are build time css-in-js libraries which just manipulate the classnames at runtime.

compiled uses the the ax function and linaria does something similar with a cx function.

Our implementation should be able to handle the safe and "not safe" cases mentioned in the RFC above by @itsdouges without needing to add additional selectors.

You can checkout this article for more details: https://medium.com/engineering-housing/css-pipeline-housing-com-ab8bc09ee4da

The corresponding implementations for linaria's cx function and the function to generate classnames can be found here and here

What we haven't been able to solve for though is long-term caching. how to ensure that we get consistent results across builds. We have done an implementation which requires storing the generated classnames in firebase DB. But we would love to do away with that approach. Looking out for suggestions and comments!

astahmer commented 1 year ago

not really related but vanilla-extract also does this with the identifier option