How to increase CSS-in-JS performance by 175x

Dominic Tobias
ITNEXT
Published in
5 min readDec 11, 2020

--

I like the convenience of CSS-in-JS especially being able to co-locate styling and improving ease-of-use when using 3rd party components. But I’m not convinced on a few of things:

  1. That hashed classes are a “must have” instead of namespaced ones, and could even be a hindrance when needing to select elements from 3rd party components, marketing, and testing suites.
  2. That using logic in your CSS is necessarily better or more readable, and is also a performance killer (more on that below).
  3. That it’s a good idea to add 25kb+ of minified JavaScript to your 10kb NPM component (not a big deal for an app however).
  4. That it can introduce new problems such as breaking because Storybook uses another version of emotion, theme Providers breaking when imported from a UI library, breaking SSR, breaking browser rules (inserting <style> tags inside body — ever wonder why emotion warns you not to use :last-child?) 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:

If you still want something dynamic you may want to consider Goober, which is a lot smaller (1kb gzip) and about twice as fast, or Stitches which is somewhere between the two options:

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 a few write ups on how using props theming litters your application with 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:

  • Use CSS Variables for theming in new projects.
  • Keep your CSS as static as possible for variations (the most important optimization).
  • Alternatively consider compile time CSS in new projects (linked above). The caveat being there can be more headaches to set them up and CSS-in-JS do offer an unrivalled developer experience.

--

--

Interested in programming and fintech: JS, WASM, Rust, Go, blockchain, L1/2, DApps, trading