The battle of CSS in JS solutions
This research was conducted by Peter ten Hoor at may 15th 2020.
History
CSS -> CSS preprocessors -> CSS modules / PostCSS -> CSS in JS / Atomic CSS
Benefits of CSS In JS
- Styling is scoped (every class will be unique).
- Only the styling that is used by the rendered components will be loaded.
- It is no longer CSS. Your entire front-end codebase can be written in Javascript, which (for example) allows you to share variables between client, node server and styling.
- Offerts the possibility to implement theming without the need of CSS variables.
- Some packages have caching which means that CSS that was used on the previous pages in your SPA will not be rerendered on the next page (client side performance improvement).
Disadvantages of CSS in JS
- No BEM.
- Some developers do not like to mix their styles and JS. They feel like it makes your code messy.
- No built in util functions like lighten, darken, rgba etc (you can use packages like polished for this).
- No linting tools.
- Existing SCSS / LESS mixins, placeholders and functions etc can not be used.
Solutions
The following packages are the most popular CSS in JS solutions:
Package | Weekly downloads | Github issues | Github stars | Last commit |
---|---|---|---|---|
Styled components | 1.875.705 | 117 / 1978 | 29.3K | 21 days ago |
JSS | 1.421.249 | 154 / 680 | 5.7K | 22 mar 2020 |
Emotion | 723.034 | 45 / 940 | 10.7K | Yesterday |
Aprodite | 296.813 | 80 / 146 | 5K | 28 Aug 2019 |
Glamor | 143.650 | 65 / 210 | 3.6K | 11 sept 2017 |
Radium | 107.485 | 66 / 516 | 7.3K | 30 dec 2019 |
Fela | 13.688 | 17 / 384 | 1.7K | 24 days ago |
Styletron | 11.236 | 18 / 143 | 3K | 11 days ago |
I will not be looking further into Aprodite, Glamor and Radium since these packages seem to be abandoned.
Because of a lack of time I will only benchmark Emotion and Styled components.
Benchmarks
Before styling
Bundle sizes
Page Size First Load JS
┌ /_app 9.62 kB 67.6 kB
├ ○ /404 2.6 kB 70.2 kB
├ λ /archive-blog 513 B 73.1 kB
└ λ /single-blog 7.32 kB 79.9 kB
+ First Load JS shared by all 67.6 kB
├ static/pages/_app.js 9.62 kB
├ chunks/commons.93f217.js 10.7 kB
├ chunks/framework.96c24f.js 40.3 kB
├ runtime/main.837877.js 6.29 kB
└ runtime/webpack.c21266.js 746 B
Stats in Chrome (incognito)
- Archive blog
- TTFB: 3ms
- 15 requests
- 215kb transferred
- 355kb resources
- finish: 88ms
- DOMContentLoaded: 25ms
- Load: 84ms
- Single blog (/emotion)
- TTFB: 3ms
- 15 requests
- 341kb transferred
- 502kb resources
- finish: 113ms
- DOMContentLoaded: 26ms
- Load: 94ms
Lighthouse performance (mobile 3G)
- Archive blog
- First contentful paint: 2.2s
- First meaningful paint: 2.2s
- Speed index: 2.2s
- First CPU idle: 2.2s
- Time to interactive: 2.2s
- Max potential first input delay: 80ms
- Single blog
- First contentful paint: 2.0s
- First meaningful paint: 2.0s
- Speed index: 2.0s
- First CPU idle: 2.1s
- Time to interactive: 2.1s
- Max potential first input delay: 70ms
Emotion
Bundle sizes
Page Size First Load JS
┌ /_app 13.8 kB 82.5 kB
├ ○ /404 2.6 kB 85.1 kB
├ λ /archive-blog 668 B 87.2 kB
└ λ /single-blog 7.59 kB 94.1 kB
+ First Load JS shared by all 82.5 kB
├ static/pages/_app.js 13.8 kB
├ chunks/commons.93f217.js 10.7 kB
├ chunks/d65d597cd3666401e623cabc91680e4367b5f208.4490fa.js 10.7 kB
├ chunks/framework.96c24f.js 40.3 kB
├ runtime/main.2e0b09.js 6.29 kB
└ runtime/webpack.c21266.js 746 B
Stats in Chrome (incognito)
- Archive blog
- TTFB: 3ms
- 18 requests
- 271 transferred
- 440kb resources
- finish: 104ms
- DOMContentLoaded: 26ms
- Load: 101ms
- Single blog (/emotion)
- TTFB: 3ms
- 18 requests
- 396kb transferred
- 587kb resources
- finish: 120ms
- DOMContentLoaded: 28ms
- Load: 114ms
Lighthouse performance (mobile 3G)
- Archive blog
- First contentful paint: 2.7s
- First meaningful paint: 2.7s
- Speed index: 2.7s
- First CPU idle: 2.7s
- Time to interactive: 2.8s
- Max potential first input delay: 40ms
- Single blog
- First contentful paint: 2.8s
- First meaningful paint: 2.8s
- Speed index: 2.8s
- First CPU idle: 2.8s
- Time to interactive: 2.8s
- Max potential first input delay: 20ms
Styled components
Bundle sizes
Page Size First Load JS
┌ /_app 11.5 kB 84.1 kB
├ ○ /404 2.6 kB 86.7 kB
├ λ /archive-blog 688 B 88.8 kB
└ λ /single-blog 7.6 kB 95.7 kB
+ First Load JS shared by all 84.1 kB
├ static/pages/_app.js 11.5 kB
├ chunks/cacd2cf700197219f0316c514326954fe8dec5ea.f7d0f2.js 14.6 kB
├ chunks/commons.93f217.js 10.7 kB
├ chunks/framework.96c24f.js 40.3 kB
├ runtime/main.2e0b09.js 6.29 kB
└ runtime/webpack.c21266.js 746 B
Stats in Chrome (incognito)
- Archive blog
- TTFB: 3ms
- 18 requests
- 272 transferred
- 444kb resources
- finish: 108ms
- DOMContentLoaded: 27ms
- Load: 104ms
- Single blog (/emotion)
- TTFB: 3ms
- 18 requests
- 398kb transferred
- 592kb resources
- finish: 135ms
- DOMContentLoaded: 25ms
- Load: 125ms
Lighthouse performance (mobile 3G)
- Archive blog
- First contentful paint: 2.8s
- First meaningful paint: 2.8s
- Speed index: 2.8s
- First CPU idle: 2.8s
- Time to interactive: 2.8s
- Max potential first input delay: 30ms
- Single blog
- First contentful paint: 2.7s
- First meaningful paint: 2.7s
- Speed index: 2.7s
- First CPU idle: 2.7s
- Time to interactive: 2.7s
- Max potential first input delay: 50ms
Concluding
Emotion has a smaller footprint than styled-components and the performance is pretty much identical. If I had more time I would also benchmark JSS, Fela and Styletron, but for now Emotion is my favorite.
Sources
Choosing a CSS in JS library
https://gist.github.com/troch/c27c6a8cc47b76755d848c6d1204fdaf#file-choosing-a-css-in-js-library-md
List of all CSS in JS solutions (outdated)
https://github.com/MicheleBertoli/css-in-js/blob/master/README.md