I like the convenience of CSS-in-JS especially being able to co-locate styling but I’m not convinced on a few of things:
- That hashed classes are a “must have” since name clashes are easily avoided in apps with good (and readable) naming, and arguably annoying on 3rd party components when you just really needed to select an element with a nice semantic class but can’t.
- That turning your CSS into a tangled mess of props and logic is all that good of an idea, which can make it less readable but also leads to terrible, and I mean terrible loss of performance.
- That you have to fiddle with build tools (restricted in Create React App for example) to try and optimize the CSS. Which by the way doesn’t do anything clever like optimize parsing times; it just minifies, removes comments, adds source maps etc.
At first you go on your merry way and don’t notice any performance problems with these “blazing fast” runtime CSS solutions. That is until you put a bunch of them on one page in Storybook.
Let’s look at the ubiquitous Button component as it has various styles and options:
We then had a bunch of simple and fast functions to compose the styles. We even optimized the static css to be it’s own chunk (sharedStaticButtonStyles):
Our Button component had the most dynamic function calls and the most elements in Storybook. Let’s see how it performs to load.
That’s around 36 seconds spent on Emotion parsing (see red underlines). The Performance tool makes it about 2–3x as long, which is still far too long.
It gets worse
I initially blamed a Tooltip component for being slow to show itself, then realized it was only when attached to a Button. It was then that I realised Emotion is parsing the CSS again on hover I guess because the Tooltip causes a re-render, and freezing the main thread for about 900ms before the tooltip could show:
You might be thinking “well my components aren’t that complicated or slow”. Even if they are twice as fast for me on a large app with many components on a page, spending 1–2 unnecessary seconds frozen on load while CSS is parsing is unacceptable. Especially on the kind of apps that I build which are highly dynamic and receiving many updates per second (which Emotion is re-parsing every render like it does on hover due to the dynamic nature of the props based styling).
So how can we make and keep things fast?
Firstly if you’re starting a new project you may want to consider compile-time CSS-in-JS solutions:
Read the docs → Install ⚠️ Work is in progress re-writing/architecting a new Babel plugin. Use the nightly at your own…
Zero-runtime CSS in JS library. Contribute to callstack/linaria development by creating an account on GitHub.
But if not no worries, you can increase performance of your existing styling by up to 175 times by making your CSS more static.
There are already various write ups on how using props theming litters your application with 100s of HOC wrappers — https://calendar.perfplanet.com/2019/the-unseen-performance-costs-of-css-in-js-in-react-apps/
And if I was starting a new project I would use CSS Variables for theming instead, I’ve already written on how to do that: https://medium.com/@dominictobias/is-it-time-to-ditch-your-react-themeprovider-e8560dad2652
But that’s not your performance bottleneck
Your performance bottleneck is in those nested function calls in your css which accept props other than the theme.
I haven’t delved into the depths of runtime CSS code but it seems that if you call a function then it basically says “I don’t know what this is going to return so I’ll recalculate it every render”.
Data attributes and CSS Variables to the rescue
Let’s change the way we do that Button component:
The interesting thing here is the use of CSS Variables, this also allows us to drastically reduce the amount of static CSS we need to write.
Here is a refactor where I kept the old functions to derive colours and sizes, but moved them from Emotion CSS to CSS Variables in an almost 1:1 refactor:
This function executes first time in about 0.002 milliseconds. Anyway let’s take a look at what the Button CSS now looks like:
Remember it took 36 seconds of parsing time? Let’s see now:
Yep, now it’s taking just over 200 milliseconds to parse the CSS. Remember with Performance recording off these numbers will be 2–3x smaller.
So in summary:
- Consider compile time CSS in new projects (linked above)
- Use CSS Variables for theming in new projects
- Keep your CSS as static as possible for variations (the most important optimization)